Zero-config SQL flight recorder that answers 'why did this Python batch job take so long?'
Project description
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?"
wherewent run python your_job.py
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.
====================================================================================================
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
====================================================================================================
Why it's different
| 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 | ❌ | ⚠️ | ✅ |
Install
pip install wherewent
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:
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
- 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.
- 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
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
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
| 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. |
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.
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:
- Raw
psycopg/psycopg2— cursor subclass or connection factory hook -
asyncpg/ async SQLAlchemy — the async execution path - Django ORM — via
connection.execute_wrapper - Generic DB-API 2.0 — a monkeypatch-free
Cursorproxy - New findings rules (N+1
SELECTdetection, 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, synchronous only (1.4 may work; async does not yet).
- Single process — no multiprocessing or async 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
—.
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wherewent-0.1.0.tar.gz.
File metadata
- Download URL: wherewent-0.1.0.tar.gz
- Upload date:
- Size: 23.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d3e2b2151375d3c2e611131da73aa8d6dcbbbb85ca573729972404e5557cd68
|
|
| MD5 |
a96a29c6326205abf247a22c5e4beb41
|
|
| BLAKE2b-256 |
83552e5baf58c39086431d24de22fea3cd19c23975ab6cb93343f52b3db41953
|
Provenance
The following attestation bundles were made for wherewent-0.1.0.tar.gz:
Publisher:
publish.yml on habibafaisal/wherewent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wherewent-0.1.0.tar.gz -
Subject digest:
9d3e2b2151375d3c2e611131da73aa8d6dcbbbb85ca573729972404e5557cd68 - Sigstore transparency entry: 2228061848
- Sigstore integration time:
-
Permalink:
habibafaisal/wherewent@ee8b45dd5ab75a2e5fc99f8bf2f41cbcd1f1809d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/habibafaisal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ee8b45dd5ab75a2e5fc99f8bf2f41cbcd1f1809d -
Trigger Event:
release
-
Statement type:
File details
Details for the file wherewent-0.1.0-py3-none-any.whl.
File metadata
- Download URL: wherewent-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
403cf8e685330b997dee722275c6765be5d92cdafb647c4b9dd23d501284892b
|
|
| MD5 |
05c151f0dc415d5d0e81ed05d2d34273
|
|
| BLAKE2b-256 |
dcb6efdb34cb530093f1ed4cd2ce9504021103a24949933160897904c68023b0
|
Provenance
The following attestation bundles were made for wherewent-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on habibafaisal/wherewent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wherewent-0.1.0-py3-none-any.whl -
Subject digest:
403cf8e685330b997dee722275c6765be5d92cdafb647c4b9dd23d501284892b - Sigstore transparency entry: 2228062293
- Sigstore integration time:
-
Permalink:
habibafaisal/wherewent@ee8b45dd5ab75a2e5fc99f8bf2f41cbcd1f1809d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/habibafaisal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ee8b45dd5ab75a2e5fc99f8bf2f41cbcd1f1809d -
Trigger Event:
release
-
Statement type: