Why django-graphex?¶
Every library carries the fingerprints of the person who built it. This page is the honest story of where django-graphex comes from — and the benchmark that tells you, in numbers, what those years of thinking bought you.
The story¶
I come from a Django REST Framework background, and that DNA runs through every
line of this project. If you have ever written a DRF ViewSet, attached a
permission_classes, reached for a pagination_class, or leaned on a
serializer to validate an incoming payload, then django-graphex is going to feel
like home. That was deliberate. I did not want GraphQL in Django to feel like a
foreign framework bolted onto your models — I wanted it to feel like the Django
you already know, with the ergonomics you already trust.
Years ago I built graphene-django-extras as a hobby. It came together over a single weekend, born out of necessity: a production project I was working on kept running into the real limitations of graphene-django at the time, and I needed a way out. So I wrote one. It solved concrete pain for concrete people, and to my surprise it found an audience. But I'll be honest with you — I never gave it all the love it deserved. Life moved on, the weekend project stayed a weekend project, and it quietly kept working for people while I looked elsewhere.
Meanwhile, the ground it stood on shifted. Over the following years, the pace of maintenance on graphene and graphene-django slowed considerably — releases stretched further apart, issues sat longer without answers. I want to be very clear about this: that is not a criticism of the people who built those projects. Graphene taught an entire generation of Django developers what GraphQL even was. It walked so the rest of us could run. I have nothing but gratitude for the work — and for the maintainers who carried it as far as they did, on their own time, for free.
django-graphex is me coming back to settle a pending debt. It is the same initial idea behind graphene-django-extras — GraphQL for Django with DRF-style ergonomics — but rebuilt the way it always deserved to be built. Modern foundations: graphql-core and Pydantic underneath, with zero graphene in the stack. The best performance I could squeeze out of it, profiled and benchmarked rather than assumed. Documentation treated as a first-class deliverable instead of an afterthought. And test coverage pushed as high as I could take it — the suite currently sits at 4,100+ tests with a hard ≥95% coverage floor enforced in CI.
What this library wants to be
The most complete Django + GraphQL experience possible — queries, filtering, pagination, mutations with validation, permissions, and subscriptions. Batteries included, one install. No Relay tax you didn't ask for, no graphene to maintain underneath you.
How it compares¶
The repository carries six distinct benchmark series. The current core33-4.1.0-f9f4eae81954959ab71e4a9037d5ba06486d6a8e results are below. The preceding core33-4.1.0-460efb9b7c29b097f2939c05e79514f2fda3242c, core33-4.0.0-bf6e1ed6ccb6d3940d253c9c83b19a436f01aa4e, core33-4.0.0-01f82ab94c86a5f35918dec4ba51deee02a58ac9, 3.1.1-source core33 and original 3.1.0 comparisons remain historical evidence, not current-package claims.
Current core33 comparison¶
The current eight portable result files measure 4.1.0 source, not a published wheel. The measured source checkout is commit f9f4eae81954959ab71e4a9037d5ba06486d6a8e (tree 5a6779d44d6b6746db2d218590a4dabbeb17f03c); it ran with the unmodified harness. The result files were committed later, so the documentation commit is not the measured source.
Each dataset has 1,000 or 2,000 authors, ten posts per author and five comments per post (50,000 or 100,000 comments). Four pinned whole stacks ran the same five operations in three cyclically rotated repetitions: 24 validated raw runs, 15 warmups and 100 timed requests per operation per run (1,800 warmups and 12,000 timed requests total). The request table shows the median of three per-run p50 values, not a pooled percentile, with request-only SQL statements. The mean table below instead shows the median of three run means. Each other published timing statistic is independently medianed across its three run statistics; per-run p95 is not a pooled 300-sample percentile. Five schema rebuild figures are per-position diagnostics, not request latencies.
All stacks use Python 3.12.11 and Django 6.0.8, but this is not an equal-core competition: GraphEx and Strawberry use graphql-core 3.3.0; Graphene and Ariadne use 3.2.13. Versions, per-library manifest and constraints SHA-256 values, Raw SHA-256 triplets, response surface, fixed SQL counts, source and seed hashes are recorded in the artifacts. The seed and measurement source are both f9f4eae81954959ab71e4a9037d5ba06486d6a8e. Digests corroborate retained local bytes; they are not signed attestation or a filesystem sandbox.
| Library | Selected whole-stack versions |
|---|---|
| graphex | django 6.0.8, django-graphex 4.1.0, graphql-core 3.3.0 |
| graphene | django 6.0.8, django-filter 25.2, graphene 3.4.3, graphene-django 3.2.3, graphql-core 3.2.13 |
| strawberry | django 6.0.8, graphql-core 3.3.0, strawberry-graphql 0.328.0, strawberry-graphql-django 0.90.0 |
| ariadne | ariadne 1.1.0, ariadne-django 0.3.0, django 6.0.8, graphql-core 3.2.13 |
Request p50 (ms) and SQL statements. Each p50 cell is the median of three run p50 values.
| Authors | Library | Flat list | Nested | Single | Filtered | Create comment |
|---|---|---|---|---|---|---|
| 1,000 | graphex | 1.0379 ms / 1 SQL | 22.8110 ms / 3 SQL | 0.5001 ms / 1 SQL | 1.4171 ms / 1 SQL | 0.4758 ms / 4 SQL |
| 1,000 | graphene | 1.8173 ms / 2 SQL | 68.4173 ms / 442 SQL | 1.0055 ms / 2 SQL | 3.7179 ms / 2 SQL | 1.1505 ms / 1 SQL |
| 1,000 | strawberry | 2.0768 ms / 1 SQL | 32.1046 ms / 3 SQL | 1.1325 ms / 1 SQL | 2.4846 ms / 1 SQL | 1.5579 ms / 8 SQL |
| 1,000 | ariadne | 1.2571 ms / 1 SQL | 43.7517 ms / 221 SQL | 0.9371 ms / 2 SQL | 1.7463 ms / 1 SQL | 0.8754 ms / 1 SQL |
| 2,000 | graphex | 1.0240 ms / 1 SQL | 22.4884 ms / 3 SQL | 0.4434 ms / 1 SQL | 1.4068 ms / 1 SQL | 0.4592 ms / 4 SQL |
| 2,000 | graphene | 1.9271 ms / 2 SQL | 68.7733 ms / 442 SQL | 1.0631 ms / 2 SQL | 5.7772 ms / 2 SQL | 1.0970 ms / 1 SQL |
| 2,000 | strawberry | 1.9475 ms / 1 SQL | 31.2644 ms / 3 SQL | 1.0990 ms / 1 SQL | 2.4796 ms / 1 SQL | 1.7528 ms / 8 SQL |
| 2,000 | ariadne | 1.2582 ms / 1 SQL | 45.9615 ms / 221 SQL | 1.0027 ms / 2 SQL | 1.8293 ms / 1 SQL | 0.8978 ms / 1 SQL |
Create-comment mean (ms). These cells are medians of three run means, not means of pooled requests and not the p50 values above.
| Comments | GraphEx mean | Ariadne mean | GraphEx less mean time |
|---|---|---|---|
| 50,000 | 0.5159 ms | 0.9268 ms | 44.34% |
| 100,000 | 0.5549 ms | 0.9382 ms | 40.85% |
For the local SQLite create-comment operation, GraphEx/Ariadne means were 0.5159 vs 0.9268 ms at 50,000 comments (44.34% less mean time for GraphEx) and 0.5549 vs 0.9382 ms at 100,000 (40.85% less mean time). The separate p50 ratios were 0.54× (0.4758/0.8754 ms) and 0.51× (0.4592/0.8978 ms), respectively. GraphEx's direct-write integrity check uses four request-internal SQL statements versus Ariadne's one; lower observed time does not mean fewer statements. GraphEx had the lowest observed mean and p50 in all ten operation/seed combinations only in this local batch, not universally. SQL counts match the preceding 4.1.0 series for every operation and library. PostgreSQL 17 CI provides correctness validation only, not a fresh PostgreSQL performance claim.
Cold import. Each artifact also records schema_import_ms: importing the library and dependencies plus one schema build in a fresh process (startup cost, not request latency).
| Library | 1,000-author seed | 2,000-author seed |
|---|---|---|
| graphex | 50.33 ms | 49.92 ms |
| graphene | 27.41 ms | 26.09 ms |
| strawberry | 330.03 ms | 346.18 ms |
| ariadne | 148.22 ms | 153.15 ms |
Graphene had the lowest cold-import observations here; these figures are not request timings. Host load fell from 10.86 to 9.24 on 16 logical CPUs during the run, so this local batch is load-sensitive. Rotation limits fixed order bias but cannot control changing load or other confounders. This is not a paired historical comparison or evidence of a speedup over the preceding series. The results carry no confidence intervals or statistical-significance claim. The prior audit did not prehash ignored original databases, migrations or benchmark environments; no alteration was observed, but retrospective full-preservation proof is unavailable. Successful child streams and HTTP bodies were not retained, and the private journal was assembled after the batch from checked receipts.
See the benchmark guide and named command for run and replay. Replay validates retained raws without measuring; run creates new seeds and a costly batch. Both require a trusted external results parent and a fresh immutable series.
Historical core33 4.1.0 comparison¶
The preceding eight portable artifacts measured 4.1.0 source 460efb9b7c29b097f2939c05e79514f2fda3242c in a separate session. They remain unchanged as historical evidence, not a paired baseline for the current batch.
Historical core33 4.0.0 comparison¶
The previous eight portable artifacts measured prepared 4.0.0 source bf6e1ed6ccb6d3940d253c9c83b19a436f01aa4e. SQL counts match the current series for all operations and libraries. The separate-session timings remain historical evidence, not a paired baseline or evidence about whether latency differences are meaningful.
Historical core33 4.0.0 pre-fragment comparison¶
The previous eight portable artifacts measured source 01f82ab94c86a5f35918dec4ba51deee02a58ac9 before the fragment-annotation correction. They remain available as historical evidence, not a paired baseline for the current timings.
Historical core33 3.1.1-source comparison¶
The following original core33 table predates the scoped SQLite mutation checker. It remains the 3.1.1-metadata migration measurement and must not be rebranded as a 4.0.0 timing result.
The earlier eight committed 3.1.1-source core33 results cover 1,000 and 2,000 authors, with ten posts per author and five comments per post. Each library runs five equivalent operations for 100 timed requests in each of three validated runs. Cells show the median of each run's p50 in milliseconds and the request-only SQL count. They do not compare identical dependency stacks: all use Python 3.12.11 and Django 6.0.8, but django-graphex and Strawberry use graphql-core 3.3.0, whereas Graphene and Ariadne use graphql-core 3.2.13. The artifacts record full selected dependency versions and separate per-library constraints hashes.
| Library | Selected whole-stack versions |
|---|---|
| graphex | django 6.0.8, django-graphex 3.1.1, graphql-core 3.3.0 |
| graphene | django 6.0.8, django-filter 25.2, graphene 3.4.3, graphene-django 3.2.3, graphql-core 3.2.13 |
| strawberry | django 6.0.8, graphql-core 3.3.0, strawberry-graphql 0.328.0, strawberry-graphql-django 0.90.0 |
| ariadne | ariadne 1.1.0, ariadne-django 0.3.0, django 6.0.8, graphql-core 3.2.13 |
| Authors | Library | Flat list | Nested | Single | Filtered | Create comment |
|---|---|---|---|---|---|---|
| 1,000 | graphex | 0.8301 ms / 1 SQL | 19.5582 ms / 3 SQL | 0.3832 ms / 1 SQL | 1.2067 ms / 1 SQL | 4.7725 ms / 4 SQL |
| 1,000 | graphene | 1.6373 ms / 2 SQL | 59.1942 ms / 442 SQL | 0.9216 ms / 2 SQL | 3.2392 ms / 2 SQL | 0.9701 ms / 1 SQL |
| 1,000 | strawberry | 1.7351 ms / 1 SQL | 29.2775 ms / 3 SQL | 0.9668 ms / 1 SQL | 2.0930 ms / 1 SQL | 1.2801 ms / 8 SQL |
| 1,000 | ariadne | 1.1141 ms / 1 SQL | 40.1715 ms / 221 SQL | 0.8606 ms / 2 SQL | 1.5429 ms / 1 SQL | 0.7818 ms / 1 SQL |
| 2,000 | graphex | 0.8335 ms / 1 SQL | 19.5867 ms / 3 SQL | 0.3834 ms / 1 SQL | 1.1910 ms / 1 SQL | 9.2149 ms / 4 SQL |
| 2,000 | graphene | 1.6338 ms / 2 SQL | 60.9607 ms / 442 SQL | 0.9403 ms / 2 SQL | 5.1500 ms / 2 SQL | 0.9816 ms / 1 SQL |
| 2,000 | strawberry | 1.7090 ms / 1 SQL | 27.8590 ms / 3 SQL | 0.9974 ms / 1 SQL | 2.0691 ms / 1 SQL | 1.2398 ms / 8 SQL |
| 2,000 | ariadne | 1.1210 ms / 1 SQL | 40.3793 ms / 221 SQL | 0.8419 ms / 2 SQL | 1.5311 ms / 1 SQL | 0.7766 ms / 1 SQL |
The Graphex create-comment figures above were measured before the prepared 4.0.0 SQLite mutation checker was narrowed to directly written rows. They remain the original 3.1.1-source measurements, not post-optimization timings.
Every reported timing statistic is the median of that statistic across three raw runs; the median of per-run p95 values is not a pooled 300-sample percentile. The five schema rebuild values are per-position median diagnostics, not a cross-library ranking. The machine's recorded platform and CPU count describe the observed host, not guaranteed CPU quietness. Raw SHA-256 digests, shared response surface, fixed SQL counts, stack versions and source hashes are in each artifact. These older local digests corroborate retained bytes; they are not signed attestation of loaded source or a filesystem sandbox.
Measurement used the unreleased migration checkout
350ae84256fdad1b1a98a6e0bef8d0f63609257f (tree
0afc136997b01b281c2a9f6c11cf062708b718ef) whose django-graphex
metadata was 3.1.1; the separately prepared seed came from
1b5b941e2555a5bffee8a109ee0a32fdef1258fd. These are not measurements
of a published 3.1.1 wheel or of a 4.0.0 release. To reproduce a fresh
profile or validate retained evidence, follow the explicit run and replay
modes in benchmarks/run_publish_core33.py
and the benchmark guide; run creates private
seeds and performs a costly new batch, whereas replay validates retained
raws without measuring. Both need a trusted external results parent.
Historical 3.1.0 comparison¶
The original comparison below is frozen with its original versions and measurements; it must not be relabeled as a core33 or 3.1.1 result.
The conditions¶
Credibility is in the conditions, so let me state all of them up front.
- Identical runtime. Same pinned Python 3.12.11 and Django 6.0.6 across all four virtual environments. Canonical pinned library versions: graphene-django 3.2.3 (+ graphene 3.4.3, django-filter 25.2), strawberry-graphql-django 0.86.4 (+ strawberry-graphql 0.320.1), ariadne 1.1.0 (+ ariadne-django 0.3.0). django-graphex is the one exception: it is installed editable from this repository, not from PyPI. These artifacts measured django-graphex 3.1.0, and record the exact source commit plus the SHA-256 of the shared dependency constraints.
- Historical version boundary. These are 3.1.0 measurements, not 3.1.1 security-patch results. Their Django 6.0.6 and GraphQL-core 3.2.11 pins remain frozen for provenance even though current library and Playground dependencies use patched versions. A comparison for 3.1.1 would require new measurements rather than relabeling these artifacts.
- Identical data. The same Django models and the same seeded dataset for
everyone: 2,000 authors, 20,000 posts, 100,000 comments, 60,000 tag
relations, generated from a deterministic seed. That is the
--authors 2000seed;run_all.shseeds half of it by default (see Reproduce it yourself). - Five semantically-equivalent operations, each written in the idiomatic
syntax of the library under test: a flat list (50 rows), a nested query
(20 authors → 10 posts → 5 comments — the N+1 stressor), a single object, a
filtered list (
icontains), and a create mutation. - Identical schema surface. All four declare the same explicit field
lists, so nobody is charged for compiling fields nobody queries — and you do
not have to take that on trust: the harness introspects the built schema back
out and records the declared fields under
surfacein every result artifact, so the rule is a thing you can diff rather than a thing I assert. On graphex the option isMeta.only_fields, which is also a security boundary — a projected column is unreadable, unorderable and unfilterable — so the reference schema demonstrates the boundary while it is being measured. - Each library in its recommended production setup. strawberry runs with
its
DjangoOptimizerExtensionenabled; graphene-django runs stock (its optimizer is a separate, unmaintained package); ariadne uses hand-written idiomatic resolvers; graphex runs on defaults. - A strict harness. Django test client, 15 warmup + 100 measured
iterations per operation, sequential single-session run, response-shape
validation before timing (a benchmark that returns the wrong data is
invalid), and SQL counts captured via
CaptureQueriesContext. macOS 26.5 arm64, 16 cores, SQLite. - Three repetitions per library, per seed; every figure is the median. One
run is not a measurement. Raw timings vary, so every artifact records the
source values and the reported statistic under
aggregation. The publisher rejects version, dataset, response, SQL, schema-surface, iteration-count or provenance drift. It does not reject a run because its timing is slower: timings are observations, not a gate. - Mutations cannot contaminate the next sample. Contract validation, SQL
probes, warmups and timed requests each run in a transaction forced to
rollback. Row counts and the database sequence are checked before and after
every library. Timing and published SQL counts cover only the GraphQL request,
not the harness's
BEGIN/ROLLBACKboundary. - The nested response is exact, not merely non-empty. Every implementation must return 20 authors, 10 posts per author and 5 comments per post, with the expected IDs, ordering and content, before timing begins.
The results¶
Per-request p50 latency (ms) and SQL queries for each operation. Lower is better on both.
| Operation | django-graphex | graphene-django | strawberry | ariadne |
|---|---|---|---|---|
| flat_list (50 rows) | 0.82 ms · 1 SQL 🏆 | 1.73 ms · 2 SQL | 1.64 ms · 1 SQL | 1.17 ms · 1 SQL |
| nested (20→10→5) | 16.28 ms · 3 SQL 🏆 | 60.04 ms · 442 SQL | 28.98 ms · 3 SQL | 42.73 ms · 221 SQL |
| single object | 0.41 ms · 1 SQL 🏆 | 0.95 ms · 2 SQL | 0.96 ms · 1 SQL | 0.85 ms · 2 SQL |
filtered (icontains) |
1.16 ms · 1 SQL 🏆 | 4.91 ms · 2 SQL | 2.02 ms · 1 SQL | 1.57 ms · 1 SQL |
| create_comment mutation | 11.77 ms · 4 SQL | 0.98 ms · 1 SQL | 1.31 ms · 8 SQL | 0.83 ms · 1 SQL 🏆 |
Startup cost is a different question, so it gets a different row¶
The number this page used to call "schema build" was timing an
import bench_schema, which pays two unrelated costs at once: loading the
library and its whole dependency tree off disk, and compiling your declarations
into a schema. Those turn out to differ by two orders of magnitude, so their
sum answers neither question. They are now measured separately.
Cold import — library + dependencies + one schema build, which is what a process actually pays at startup:
| Metric | django-graphex | graphene-django | strawberry | ariadne |
|---|---|---|---|---|
| Cold import, 2,000-author run | 10.21 ms | 10.45 ms | 93.24 ms | 47.88 ms |
| …at the 1,000-author seed | 9.30 ms | 10.81 ms | 98.70 ms | 45.58 ms |
Read it as an order of magnitude: in this historical 3.1.0 series graphex and graphene-django were indistinguishable around 10 ms (the current core33 series above measures them apart), ariadne roughly 5× them, strawberry roughly 10×. It still does not say which of the first two is faster, and it never could — the two are one millisecond apart while the eight canonical cold-import sample sets span roughly 9–24 % from minimum to maximum relative to their median. Earlier revisions of this page named opposite winners there; neither should have been published.
What that row does not measure is how fast each library compiles a schema.
With the dependency tree already imported, rebuilding the same schema costs
roughly 3 ms (graphex), 4 ms (graphene-django), 6 ms (strawberry), 2 ms
(ariadne) — so strawberry's 106 ms is overwhelmingly the cost of importing
strawberry, not of building anything. Those figures are in the artifacts under
schema_rebuild_samples_ms, kept as a raw series rather than reduced to a
single number, because they are a diagnostic and not a comparison:
re-executing declarations perturbs each library's process state differently.
django-graphex's series climbs measurably across repeated rebuilds where
ariadne's is flat, which is a property of graphex worth knowing and not a
property you can rank libraries by. Read the series down one column, never
across.
The bias this row used to carry is gone. run_all.sh seeds the database
under the graphex interpreter, which left graphex's imports hot while the other
three were measured cold — a bias in graphex's favour on the one row where the
libraries are closest. It now warms every virtualenv before measuring any of
them. Each canonical artifact retains all three import samples so you can
inspect that spread directly. The per-operation rows never had the problem: p50
over 100 iterations after 15 warmups is long past any import cost.
Every operation cell above is the p50_ms / sql_queries pair sitting in
benchmarks/results/2x_<lib>.json — each the median of three runs, recorded
in the file under aggregation — and those four files are tracked in the
repository — a clone or a git archive export contains them, and you can read
them on GitHub without cloning anything. (The published sdist ships only the
library, its tests and the docs, so the benchmark tree is not in the tarball.)
Open them, diff them against your own run, and hold this table to what they say.
What the numbers actually mean¶
The nested row is the one that matters at scale. For the same response, graphene-django fires 442 SQL queries where graphex fires 3 — a textbook N+1 explosion that graphex avoids by prefetching the relation tree. And here's the part that's easy to miss: this ran on local SQLite, which understates the gap. In production, against Postgres over a network, every one of those 442 round-trips pays real latency. The 16.28 ms vs 60.04 ms measured here becomes a far wider chasm the moment there's a wire between your app and your database.
The scaling story is just as telling. Doubling the dataset (from 1,000 to
2,000 authors) left graphex's filtered operation flat: 1.13 ms → 1.16 ms —
it's O(page): no unconditional COUNT, and a LIKE + LIMIT early exit.
Over the same doubling, graphene-django's filtered operation climbed from
3.25 ms → 4.91 ms — it's O(table), because its count scans the whole
thing. The lead doesn't just hold as your data grows; it widens.
Every number in that paragraph is the filtered operation's p50, read from
four tracked artifacts: benchmarks/results/graphex.json and
benchmarks/results/graphene.json for 1,000 authors, and the 2x_ files beside
them for 2,000 (benchmarks/README.md has the reseed recipe). graphex's pair
rose by 0.03 ms across a doubling — which is noise, and is exactly why the claim
here is flat rather than slower. graphene's rose by 1.66 ms, far beyond
that noise.
Honest caveats — because you should trust numbers that admit their limits
- The cold-import row is still the weakest number on this page, even
with its bias fixed. It is one sample per process; the current artifacts'
minimum-to-maximum spread is roughly 9–24 % relative to their medians, so
an order of magnitude is a finding there and a millisecond is not, in
either direction. The source samples are recorded under
aggregation. - The rebuild series is a diagnostic, not a ranking. graphex climbs
across repeated in-process rebuilds where ariadne is flatter, because an
append-only registry of declared types makes every rebuild re-walk dead
generations. A deployment pays none of that repeated-run effect: the walk
happens once per process, at
AppConfig.ready(). It is why the series ships as raw samples rather than a comparable figure. - graphex's parse + validate cache shines on repeated documents — which is the real-world API pattern, where the same operations run over and over.
- ariadne's numbers are hand-written raw resolvers. That's idiomatic for ariadne, and it's fast — but it carries none of the framework services the other three provide out of the box: validated filter inputs, pagination wrappers, error envelopes. It's a fair comparison of what each tool is, not a like-for-like feature comparison.
- The security guards are in these numbers. The
projection boundary runs one
shared predicate on two paths: the filter guard consults it while the
schema builds, and the ordering allowlist consults it per request on the
nested window path.
benchmarks/guard_cost.pycan profile that predicate locally, but its diagnostic is not published as a canonical timing. There is no switch to turn the boundary off, so nothing here is a "guards off" number and no A/B against one exists.
Reproduce it yourself¶
I don't want you to take my word for any of this. The complete harness lives in
the repo under benchmarks/,
and so do the eight result artifacts every number on this page was read from —
results/<lib>.json for the 1,000-author seed and results/2x_<lib>.json for
the doubled one, tracked rather than gitignored precisely so you can open them
before you run anything. The README there documents the full operation contract
and the fairness rules. Recreate the exact pinned environments, then run the
validated median publisher from the repository root:
cd benchmarks && ./setup_envs.sh && cd ..
python benchmarks/run_publish.py --authors 1000 2000 --runs 3
It recreates each seed, rotates library order, validates the response contract, versions, schema surface and SQL counts, then atomically publishes all eight medians. A failed raw run leaves the existing canonical artifacts untouched.
The direct versions live in benchmarks/versions.env; the complete transitive
freeze lives in benchmarks/constraints.txt. Every result stores commit and
measurement_tree for the actual local commit and tree that were measured,
plus constraints_sha256 for the dependency graph. delivery_base_commit is
only the public ancestor from which the JSON was delivered and validated; it is
not the measured state and does not claim byte, tree or semantic equivalence
with it. The benchmark README documents and CI validates that boundary without
trying to resolve the local measurement commit.
After priming uv's cache, replay without network access:
BENCH_OFFLINE=1 benchmarks/setup_envs.sh
python benchmarks/run_publish.py --authors 1000 2000 --runs 3
Offline mode fails clearly when the cache lacks a required distribution instead of silently resolving a different environment.
One last, honest word
Every one of these libraries made different trade-offs, and every one serves its users well. graphene taught us GraphQL; strawberry brought a beautiful typed, modern API; ariadne gives you schema-first purity and total control. Performance is only one dimension — pick the tool that fits your project and your team. But if the question you're asking is "how do I get performance with batteries included?", then this — right here — is the answer the data gives.