
Security News
Ruby's Bundler 4.0.18 Extends Cooldown to bundle lock and bundle cache
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.
wherewent
Advanced tools
Zero-config SQL flight recorder that answers 'why did this Python batch job take so long?'
A zero-config recorder that answers "why did this Python batch job take so long?"
wherewent run python your_job.py
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.
====================================================================================================
wherewent — SQL flight recorder
----------------------------------------------------------------------------------------------------
wall: 26.15s cpu: 25.41s (97% CPU busy) queries: 20,004 commits: 20,001 rollbacks: 1
in-DB time: 5.46s (20.9% of wall; app-observed: includes network+driver+server)
commit time: 9.06s total rows: 20,000
recording added ~1.81s (~6.9% of wall)
====================================================================================================
QUERY GROUP CALLS TOTAL MEDIAN CALL SITE
----------------------------------------------------------------------------------------------------
INSERT INTO events (name, value) VALUES (?, ?) 20,000 5.46s 0.24ms demo/naive_job.py:65 in main
SELECT count(*) AS count_1 FROM events 1 0.00s 0.16ms demo/naive_job.py:71 in main
====================================================================================================
FINDINGS
----------------------------------------------------------------------------------------------------
1. [R1+R2] commit-per-row loop
20,000 calls x 0.24ms median ~= 5.5s = 21% of 26.1s wall, at demo/naive_job.py:65. Batch it.
20,001 commits for 20,000 rows (1.0 rows/commit), 9.1s in commit = 35% of wall. Batch to 1,000+ rows/txn.
~= 14.5s attributable
====================================================================================================
| Sampling profilers | APM / tracing | wherewent | |
|---|---|---|---|
| Zero code changes | ✅ | ❌ | ✅ |
| Groups queries by shape | ❌ | ⚠️ | ✅ |
| Blames your call site | ⚠️ | ⚠️ | ✅ |
| Tells you the fix | ❌ | ❌ | ✅ |
| Runs anywhere, no server | ✅ | ❌ | ✅ |
| Works on a Ctrl-C'd partial run | ❌ | ⚠️ | ✅ |
pip install wherewent
That's it — the recorder is pure standard library. You only need SQLAlchemy because your job already uses it.
Wrap any command. Your script runs completely unmodified — no imports, no decorators, no config:
wherewent run python your_job.py --some arg
wherewent run python -m your_package
wherewent run --save run.json python your_job.py # also dump machine-readable JSON
SIGUSR1 (kill -USR1 <pid>) for a partial snapshot mid-run,
or run with WHEREWENT_INTERVAL=30 to print one every 30s. The job keeps going.AsyncSession / AsyncConnection).git clone https://github.com/habibafaisal/wherewent && cd wherewent
pip install -e ".[dev]"
wherewent run python demo/naive_job.py # watch the R1+R2 finding fire
python demo/benchmark.py # naive vs fixed, with the overhead gate
"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:
# Zero-config: name a function; every top-level call is one unit
wherewent run --unit-function myapp.jobs:process_receivable python run.py
# Or mark the unit in code (same machinery, same report)
import wherewent
for receivable in book:
with wherewent.unit("receivable"):
process(receivable)
UNIT: myapp.jobs:process_receivable (1,203 units)
----------------------------------------------------------------------------------------------------
median duration 341 ms queries/unit 135 (median)
commits/unit 1.0 rows/unit 46.0
GROWTH
units 1–100 220 ms/unit
units (last 100) 379 ms/unit
queries 1–100 98 queries/unit
queries (last 100) 171 queries/unit
trend +72% slower over the run
query trend +74% more queries/unit over the run ← R6 fires
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.
PYTHONPATH sitecustomize shim — no
changes to your code, no wrapper imports.event.listen(sqlalchemy.engine.Engine, ...) — so
every engine your app creates is captured automatically, config-free.IN-lists
and multi-row VALUES collapse, so a million distinct inserts become one honest row.| Rule | Fires when | Tells you |
|---|---|---|
| R1 — chatty group | > 1,000 calls, > 10% of wall, median < 5ms | A fast query is called too many times — batch it (executemany / IN-list / JOIN). |
| R2 — commit-per-row | > 100 commits, < 10 rows/commit, > 5% of wall in commit | You're committing per row — batch to 1,000+ rows per transaction. |
| R3 — DB-wait bound | in-DB time > 60% of wall, CPU busy < 30% | The job is round-trip bound, not compute bound. |
| R4 — co-occurring pattern | ≥ 2 query groups fire from the same function AND the pattern scales — many queries/iteration across many iterations, or > 10% of wall once one-time setup is excluded | Several queries fire together every iteration (SELECT + UPDATE + INSERT) — collapse them into one round-trip. Clusters by function, not by line, so a helper that issues its statements on three different lines is still seen as one operation. One-shot (calls == 1) statements are excluded — they're fixed cost, and R5's job. Reports estimated queries-per-iteration, and flags patterns that scale even when a bounded run's clock hides them. |
| R5 — one-shot heavyweight | a single calls==1 statement > 15% of wall or > 10s absolute | One statement is a huge fixed cost. R1/R3/R4 all look for chattiness and miss it — R5 catches the single most fixable line. The absolute floor matters: 20s is worth cutting whether it's 24% of a sampled run or 1% of the full one. |
| R6 — rising per-unit cost | per-unit time or queries/unit climbs ≥ 1.5× from the first 100 units to the last 100 (needs --unit-function/wherewent.unit()) | Cost per item grows as the run progresses — accumulating state, unbatched history reads, or a list that grows each loop. Reports the slope (queries/unit early vs late), so a compute-bound job whose query cost is growing still gets caught. |
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.
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:
For each receivable:
For each audit event ×23:
SELECT chain_state → SELECT payload → INSERT payload → INSERT audit_event → UPDATE chain_state
Finding: serialized audit-append loop — 23 repetitions/receivable, ≈115 statements/receivable,
58% of DB activity, at process_receivable → emit_firing → append_event.
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.psycopg / psycopg2 — cursor subclass or connection factory hookasyncpg (outside SQLAlchemy) — the async execution pathconnection.execute_wrapperCursor proxySee CONTRIBUTING.md for the backend contract and the < 15% overhead
gate that every capture path must pass.
asyncpg outside
SQLAlchemy is not attributed yet.—.≈) inferred from co-occurring query
counts — shown only when the signal is strong, never guessed.--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.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.calls and total_time remain exact.These are the honest edges of a validation prototype, not permanent walls — see the roadmap.
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.
MIT © 2026 Habiba Faisal
FAQs
Zero-config SQL flight recorder that answers 'why did this Python batch job take so long?'
The pypi package wherewent receives a total of 59 weekly downloads. As such, wherewent popularity was classified as not popular.
We found that wherewent demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.

Security News
During a UK cyber test, a Mythos 5 agent used sockpuppets, social engineering, and prompt injection to try to get a maintainer to merge malware.

Company News
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.