
Wherewent
io.github.habibafaisalPublished by GitHub account habibafaisalv0.4.1Updated Oct 24, 2026
Zero-config SQL flight recorder for slow Python/SQLAlchemy jobs: exact call site, count, and fix.
Overview
Runs a Python/SQLAlchemy job under a zero-config SQL recorder and returns why it was slow: query groups, call sites, counts and fixes.
- What you can do
- Run a Python or SQLAlchemy job under the recorder and get structured findings on why it was slow
- Read back enriched findings from a JSON report already saved by a previous run
- Each finding carries the fix, call site, call count, share of wall time and an evidence object
- Reports query groups by shape, commit patterns, in-DB time and rising per-unit cost
- When to use it
- Useful when a Python batch or SQLAlchemy job is slow and you want the assistant to pinpoint the exact file:line and calling pattern instead of reading raw query logs. Also handy for analysing a saved run report without re-running the job.
- Requirements
- Runs as a local stdio process on the user's machine (desktop only). Install the PyPI package with the mcp extra, which pulls in the MCP SDK; the core recorder is pure standard library. The target job must use SQLAlchemy 2.x (sync or async); no API keys or environment variables are declared.
Installation
In SourceWeft
- Open Wherewent in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
wherewent
Where did the time go? Find out in one command.
A zero-config recorder that answers "why did this Python batch job take so long?"
Run it from your shell — or as an MCP server an AI agent invokes directly.
The 0.4ms query that costs you 5 minutes
A query can be individually fast — 0.4ms — and still sink your job, because it's
called 500,000 times from a single line of code. Your app burns 300 seconds on
round-trips while Postgres itself only worked for 80. Every profiler you've tried shows
you "time spent in psycopg" and stops there.
wherewent shows you the calling pattern. It groups queries by shape, counts how
often each shape ran, sums the wall time, and points at the exact file:line in your
code that fired it — then tells you, in plain English with the arithmetic shown, what to
do about it.
Why it's different
Install
That's it — the recorder is pure standard library. You only need SQLAlchemy because your job already uses it.
Use it
Wrap any command. Your script runs completely unmodified — no imports, no decorators, no config:
- The report prints to stderr at exit; your job's own stdout/stderr pass through untouched.
- Ctrl-C still produces a report. Sampling the first 5 minutes of a 14-hour job is the main use case — partial data is the point.
- Peek without stopping. Send
SIGUSR1(kill -USR1 <pid>) for a partial snapshot mid-run, or run withWHEREWENT_INTERVAL=30to print one every 30s. The job keeps going. - Works on async SQLAlchemy. Queries run inside a greenlet with no user frames on the
stack, so naive stack-walking blames nothing; wherewent attributes them to your real call
site anyway (
AsyncSession/AsyncConnection). - It can never crash or corrupt your job. Every hook body is wrapped so the recorder fails silent rather than taking your run down with it.
- It never records your data. Only query shapes and counts are kept — literal values and bind parameters are stripped before anything is stored.
Try the built-in demo
Use it as an MCP server (agent-native)
wherewent ships a Model Context Protocol (MCP) server, so
an AI agent can invoke it directly the moment a job is slow and get back machine-readable findings
— instead of reading raw query logs and reasoning its way to the same conclusion. It is listed in
the official MCP Registry as
io.github.habibafaisal/wherewent.
Install with the [mcp] extra (this pulls in the MCP SDK; the core recorder stays pure-stdlib)
and run the stdio server:
Transport: stdio. Tools exposed:
Each finding carries fix, call_site, calls, wall_fraction, and an evidence object an
agent can act on and cite. Wire it into any MCP client (e.g. Claude Desktop) via config:
A Dockerfile at the repo root builds this same stdio server for container-based MCP hosts.
Name your unit of work
"81,749 queries" is hard to judge. "135 queries per receivable" tells an engineer instantly that the architecture is chatty. Name the unit your job processes and wherewent reports the economics of one — median duration, queries/commits/rows per unit, and how the cost trends as the run progresses:
R6 fires on either slope. That matters for a compute-bound job: if the clock stays flat but
queries/unit climbs, the duration trend reads flat and only the query trend exposes the problem —
so wherewent reports both and says plainly that the pattern is a scalability risk rather than the
current wall-clock bottleneck.
The growth trend is why a sampled run is honest: it shows cost-per-unit rising, so you know the full run will be worse than a linear extrapolation — the thing a totals-only profiler can never tell you. Per-unit counts are exact even under concurrent async units; nothing but shapes and counts is ever recorded.
How it works
- Injects itself into the target process via a
PYTHONPATHsitecustomize shim — no changes to your code, no wrapper imports. - Listens at the class level —
event.listen(sqlalchemy.engine.Engine, ...)— so every engine your app creates is captured automatically, config-free. - Normalizes each statement into a query group: literals, bind params,
IN-lists and multi-rowVALUEScollapse, so a million distinct inserts become one honest row. - Resolves the call site by walking the stack past library frames to the first line of your code — cheaply: cached by filename, full stacks only for the first 5 samples per group, so the hot path stays cheap enough to hit its overhead budget.
- Fires deterministic findings from three rules, each showing its arithmetic.
The findings engine
Findings that share a root cause merge (e.g. R1+R2), everything under 5% of wall is
suppressed, and at most the top 3 are shown — ranked by seconds attributable. R4 catches
the case a per-group threshold can't: an N+1 pattern spread across a SELECT + UPDATE + INSERT
that individually look innocent but fire as one unit each loop — and, since v0.3, it fires on
patterns that scale even when one-time setup costs make them look small on a short sample run.
Every number is honest. Query times are labelled app-observed (they include network,
driver, and server time — not just Postgres). Anything that can't be measured prints —,
never a guess. wherewent even times its own hooks and reports the overhead it added.
Roadmap — help wanted 🙌
wherewent is built to grow beyond SQLAlchemy. Seven of its eight modules —
normalization, call-site resolution, the stats model, the rules engine, the report, the
CLI, and the injection shim — are already framework-agnostic. They operate on a plain
RunSnapshot of query events. Only recorder.py, which binds SQLAlchemy's event system,
is framework-specific.
That means a new backend is a well-contained contribution: capture query
start/end/rowcount/txn events from another driver, feed the same RunSnapshot, and the
entire findings-and-report pipeline works for free. Good first backends:
- Async SQLAlchemy — call-site attribution through the greenlet boundary (v0.2.0)
- Work-unit-aware profiling — per-unit economics + growth trend (v0.3.0)
- Execution-pattern findings (the next big one — help wanted) — today wherewent clusters the queries that fire together each iteration (R4). Next: reconstruct the ordered, possibly nested workflow behind them and name it, e.g. This is a real step past ordinary N+1 detection (Sentry/Scout find repeated single-shape queries; this would find multi-operation workflows spanning several SQL shapes and functions): read→modify→write loops, serialize→insert→commit per item, whole-state snapshots after every mutation, growing-history scans, and CPU rising with item position. Needs an ordered per-unit event log + repeated-subsequence mining, kept under the overhead gate.
- Raw
psycopg/psycopg2— cursor subclass or connection factory hook - Raw
asyncpg(outside SQLAlchemy) — the async execution path - Django ORM — via
connection.execute_wrapper - Generic DB-API 2.0 — a monkeypatch-free
Cursorproxy - More findings rules (lock-wait, seq-scan heuristics)
See CONTRIBUTING.md for the backend contract and the < 15% overhead
gate that every capture path must pass.
Limitations (today)
- SQLAlchemy 2.x (sync and async ORM/Core; 1.4 may work). Raw
asyncpgoutside SQLAlchemy is not attributed yet. - Single process — no multiprocessing fan-out.
- Query times are app-observed (network + driver + server), by design.
- Commit timing is obtained by wrapping the dialect's commit; if that wrap fails it prints
—. - Per-iteration ratios are estimates (labelled
≈) inferred from co-occurring query counts — shown only when the signal is strong, never guessed. - Per-unit counts (
--unit-function/wherewent.unit()) are exact even under concurrent async units; per-unit duration is wall time and may overlap when units run concurrently — the common sequential-loop case is exact. - R6's attributed seconds are a deterministic lower-bound estimate, not a measurement. The excess queries per unit are priced at the run's mean per-query DB time, so if the extra queries are cheaper than average the true cost is higher (and vice versa). It is computed from exact integer query counts rather than the clock, so it is reproducible run to run — but R6's claim is the slope, not the seconds.
- ORM flush attribution. Queries emitted by a
session.flush()/commit()all resolve to that one call site, so R4 can group unrelated writes under a single "workflow". When a cluster's writes share one source line, wherewent labels it as possibly a single flush rather than claiming you can collapse it — it will not tell you to batch something already batched. - Per-group median is a bounded sample median (reservoir of 5,000 executions per group) so
memory stays flat on million-query runs.
callsandtotal_timeremain exact. - Commit vs rollback time are reported separately. SQLAlchemy's pool issues a rollback on every connection check-in, so rollback time is labelled (incl. pool resets) and is never folded into commit time.
- Findings describe where the time goes and how it scales — on a CPU-bound run they say so explicitly, rather than implying that fixing the SQL will speed up this run.
These are the honest edges of a validation prototype, not permanent walls — see the roadmap.
Contributing
Contributions are very welcome — new backends, new rules, docs, bug reports. Start with
CONTRIBUTING.md, open an issue to discuss anything substantial, and
run pytest && python demo/benchmark.py before you push.
License
MIT © 2026 Habiba Faisal
Source: README.md at commit 5ec830e
Tools
0Version history
1- v0.4.1LatestSep 16, 2026
Score
50% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 days25 pts
Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.
- Auth declared10 pts
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts