Performance#
Every claim on this page is falsifiable on purpose: judged by published,
reproducible benchmark numbers, never adjectives. Full methodology and
every honest caveat live in benchmarks/README.md in the repository;
this page summarizes the headline results.
Methodology: oha, one warm-up
request per target, then the real measurement. Four scenarios: a minimal
JSON echo, a DB-backed list read, a server-rendered HTML page, and static
file serving. Each framework served by its own idiomatic server. Full
identities of the comparison frameworks are in benchmarks/README.md in
the repository. The original pass below used a single run at 10s
duration / 30 connections; benchmarks/run.py --runs N (median + min/max
spread across N repeated runs) was added since, and the recommended --
and now published -- methodology is 5 repeated runs at 200 connections
(--duration 10s --connections 200 --runs 5 on Windows; --duration 5s
on Linux CI, shorter only to keep CI job time down). Both environments
are published side by side below, each labeled; see
benchmarks/README.md's "Running it" and "Running it in CI (Linux)"
sections for exact invocations and honest caveats.
vs. other popular frameworks#
Windows (dev machine), original methodology -- kept for history#
Hayavega, WSGI transport, 30 concurrent connections, single run
| Scenario | Hayavega (WSGI) | Hayavega (ASGI) | Async framework | Sync framework |
|---|---|---|---|---|
json_echo |
3520 | 2153 | 1490 | 1463 |
db_list |
920 | 906 | 634 | 452 |
template_render |
799 | 763 | 688 | 332 |
static_file |
1384 | 1108 | 646 | 921 |
req/s, higher is better. Single-machine, single-run — see
benchmarks/results/RESULTS.md in the repository for p50/p90/p99 and
success rate on every row. Kept rather than deleted: this is the
original, broadest comparison referenced elsewhere in this project (e.g.
the "environment ceiling" finding in INDUSTRY_LEADERSHIP_ROADMAP.md),
and the re-measurement below uses a different methodology (200
connections, 5 repeated runs), not a direct apples-to-apples update of
this exact table.
Windows (dev machine), re-measured under the repeated-run methodology#
| Scenario | Hayavega (WSGI) | Hayavega (ASGI) | Async framework | Sync framework |
|---|---|---|---|---|
json_echo |
3784 | 2445 | 1355 | 1956 |
db_list |
1804 | 1701 | 592 | 400 |
template_render |
1340 | 1179 | 659 | 266 |
static_file |
1300 | 1104 | 529 | 1022 |
req/s, median of 5 runs at 200 connections — see
benchmarks/README.md's "Running it" section for the exact invocation.
This is disclosed as a re-measurement, not claimed as a new fix. No
framework code changed between the original pass above and this one --
db_list and template_render moved substantially higher for Hayavega's
WSGI transport (roughly +96% and +68% respectively) under this
methodology. The most likely explanation is the higher connection count
(200 vs. the original 30): more concurrent requests queue behind the
same database/thread-pool resources, which raises aggregate throughput
up to a point rather than leaving it flat, the same kind of
concurrency-sensitive behavior this document's own "environment ceiling"
finding already describes for this machine. json_echo and
static_file moved by smaller, single-digit-to-low-double-digit margins
in both directions, consistent with ordinary run-to-run variance at a
different concurrency level rather than a methodology artifact specific
to those two scenarios.
Linux (CI), repeated-run methodology#
Produced by the benchmark-linux CI job (see benchmarks/README.md's
"Running it in CI (Linux)" section), ubuntu-latest, --duration 5s
--connections 200 --runs 5, GitHub Actions run 43223535309 (re-measured
2026-11-12, after this session's static_file regression fix -- see "The
Rust runtime vs. a reference WSGI server" below; superseding the
previously published run 42812292945, kept here only as the superseded
number since CI runner noise alone does not explain every difference,
most visibly static_file):
| Scenario | Hayavega (WSGI) | Hayavega (ASGI) | Async framework | Sync framework |
|---|---|---|---|---|
json_echo |
10208 | 16800 | 6904 | error1 |
db_list |
74 | 6762 | 1077 | error1 |
template_render |
80 | 4905 | 1099 | error1 |
static_file |
8678 | 13784 | 1812 | error1 |
req/s, median of 5 runs at 200 connections on a GitHub Actions
ubuntu-latest runner — not the same machine as the Windows
numbers above, so the two environments are not a fair head-to-head
against each other, only each internally consistent. static_file's
WSGI number (2342 → 8678, a real 3.7×) is the clearest
Linux-side confirmation that the static_file regression fix below is a
genuine win, not just a Windows dev-machine artifact.
One more honest gap, disclosed rather than smoothed over, and more
pronounced in this run than the one it supersedes: Hayavega's WSGI
transport (waitress) is dramatically slower than its own ASGI transport
on this specific CI runner for db_list/template_render -- roughly a
91x gap (74 vs. 6762 req/s) and a 61x gap (80 vs. 4905 req/s)
respectively, computed entirely from the Linux numbers above (up from a
45x/29x gap in the superseded run). It's the size of this WSGI/ASGI gap
that's unusual, not a cross-machine throughput comparison: on Windows the
same two transports were close to each other (see the table above), so
this split is specific to the Linux runner. The gap occurs at a 100%
success rate -- no timeouts or errors are depressing the WSGI figure --
but with genuine tail-latency contention, e.g. db_list p90/p99 around
3.9s/4.9s vs. p50 around 81ms. This was not root-caused as part of
publishing these numbers; it is published as-is, honestly, as a lead for
a future investigation rather than papered over or excluded, and its
growth between two separate runs is itself a data point for that future
investigation. benchmarks/README.md's "Performance regression gate"
section cross-references this same anomaly as the more likely (though
still unconfirmed) explanation for db_list's ~10x between-VM noise on
the CI performance gate, rather than that noise being disk-I/O-bound.
Hayavega leads every published scenario on both transports on Windows
against FastAPI and Django, the two comparison frameworks above. On
Linux CI, Hayavega's ASGI transport leads every scenario; the WSGI
transport leads static_file but trails the async framework on
db_list/template_render on this specific runner -- the same gap
disclosed immediately above, published honestly rather than rounded up
to a clean sweep (json_echo is close: 10208 vs. 6904, still a Hayavega
WSGI win here, unlike the superseded run where it was closer still).
Against the four harder targets below (a raw-ASGI baseline, Granian,
Robyn, Blacksheep), that clean sweep does not hold -- see the disclosure
under that table.
Linux (CI), eight-way comparison: Granian, Robyn, Blacksheep, and a raw-ASGI baseline#
The same four harder targets as the Windows eight-way table below (a
raw-ASGI baseline, Granian, Robyn, Blacksheep), from the same
benchmark-linux CI run (43223535309) as the table immediately above
-- one coherent dataset, not numbers stitched together from different
runs. This closes the gap the benchmark-credibility plan's own final
review flagged and deferred: this data existed in a CI artifact and had
never been written up before.
| Scenario | Hayavega (WSGI) | Raw ASGI | Granian | Robyn | Blacksheep |
|---|---|---|---|---|---|
json_echo |
10208 | 30915 | 75665 | 41728 | 27688 |
db_list |
74 | 8867 | 9886 | 9474 | 8147 |
template_render |
80 | 9342 | 10382 | 8935 | 6089 |
static_file |
8678 | 32488 | 84250 | 29660 | 4448 |
req/s, median of 5 runs at 200 connections, 100% success on every cell.
Disclosed, not rounded up: Hayavega does not win any scenario against
this harder field on Linux, unlike db_list on Windows. Granian leads
every scenario here by a wide margin (7.4× on json_echo,
9.7× on static_file), and even Blacksheep -- the one other full
Python framework in this table, not just a bare baseline or a Rust-core
runtime -- beats Hayavega's WSGI transport on every scenario except
static_file, where Hayavega's new caching (see below) gives it a real
2× lead. This is the honest trade of comparing WSGI's synchronous,
thread-per-connection model against async/Rust-core servers on a runner
where that model's weaker side (shown immediately above: the WSGI/ASGI
split) is already more pronounced than on Windows. Hayavega's ASGI
transport (not shown in this table; see the table above) would close or
reverse most of this gap -- 16800/6762/4905/13784 against this
same field would lead db_list and template_render outright and be
competitive on the rest -- but that comparison isn't run here yet; adding
Hayavega (ASGI) to this specific eight-way table is tracked as a real
follow-up, not silently assumed.
Windows (dev machine), eight-way comparison: Granian, Robyn, Blacksheep, and a raw-ASGI baseline#
Four harder targets, added for credibility rather than flattery: a
raw-ASGI baseline with no framework at all, that same baseline served by
Granian (a Rust-core server), Robyn (a Rust-core framework that hosts
itself), and Blacksheep (a fast pure-Python ASGI framework). Same
methodology as the re-measured table above (--duration 10s --connections
200 --runs 5, same Windows dev machine). Full detail (p50/p90/p99,
exact invocations, port assignments, a flaky first run caught and
discarded on retry) is in benchmarks/README.md's "Eight-way comparison"
section.
| Scenario | Hayavega (WSGI) | Raw ASGI | Granian | Robyn | Blacksheep |
|---|---|---|---|---|---|
json_echo |
4368 | 4687 | 5171 | 14303 | 4327 |
db_list |
1926 | 1108 | 1439 | 1491 | 1062 |
template_render |
1441 | 1149 | 1506 | 1446 | 989 |
static_file |
1579 | 4863 | 5375 | 4252 | 1178 |
req/s, median of 5 runs at 200 connections, 100% success on every cell.
A disclosed, unreconciled gap worth flagging: Hayavega (WSGI)'s own
json_echo number here (4368) differs by about 16% from the "re-measured"
table above (3784), even though both claim the identical methodology
(--duration 10s --connections 200 --runs 5) on the same Windows dev
machine. Several other numbers between those two tables drift by
15-30% the same way. This is treated the same as every other noise
finding already disclosed in this document: a real, session-to-session
environment property of this shared dev machine, not hidden or smoothed
over. No attempt is made to reconcile the two tables' numbers against each
other -- they are each internally consistent (100% success rate, same
invocation), just not directly comparable session-to-session.
Disclosed, not rounded up: Hayavega does not win every scenario here.
It wins db_list outright against all four, by 1.3–1.7×, and
effectively ties template_render with Robyn (0.3% apart) while trailing
Granian there by a real 4.5%. But on json_echo and static_file -- the
two scenarios with the least work per request, exactly where a thinner
stack has the most room to show it -- Hayavega loses to Raw ASGI,
Granian, and Robyn by a real margin, up to Robyn's decisive
3.28× on json_echo. Blacksheep, the one new target that is
also a full Python web framework rather than a bare baseline or a
Rust-core runtime, loses every scenario to Hayavega except an
effectively-tied json_echo (0.9% apart).
The Rust runtime vs. a reference WSGI server#
| Scenario | Rust runtime | Reference WSGI server | Ratio |
|---|---|---|---|
json_echo |
10362 req/s | 4807 req/s | 2.16× |
db_list |
3926 req/s | 1267 req/s | 3.10× |
template_render |
2692 req/s | 1328 req/s | 2.03× |
static_file |
7242 req/s | 4980 req/s | 1.45× |
_Re-measured 2026-11-12 (two runs, oha -z 10s -c 30, 100% success every
target every run). static_file was briefly a confirmed, reproducible
loss (0.41–0.54×) in an earlier re-measurement the same
day -- real profiling (py-spy, not a guess) found hayavega.static's
serve() calling Path.resolve() (a real filesystem syscall) on every
single request, which this project's own existing template-caching
precedent (templates.py's mtime-invalidated _compile_cached) already
established the fix for: cache the resolved path and the file's
content/ETag, invalidated by stat()'s mtime, so a repeat request for an
unchanged file skips both the path-resolution and the file-read/hash work
entirely. Fixed in the same change as this re-measurement -- see
src/hayavega/static.py and tests/test_mvp_polish.py's
test_static_routes_caches_* tests. Every scenario is now a genuine win,
not three wins and one disclosed loss.
100% success rate, both targets, every run — and this reflects the hardened server (request-smuggling, response-splitting, and Slowloris defenses all included) with zero measured performance cost. See Rust runtime.
More Rust-acceleration wins since#
The two results above are the original, broadest ones; a handful of
narrower candidates cleared the same "benchmark first, wire in only on
a decisive win" bar since. Full reasoning and the complete rejected-
candidate history live in RUST_CORE_REWRITE_ROADMAP.md.
| Candidate | Result | Status |
|---|---|---|
SerializedConnection (one Rust-serialized SQLite connection, a hot read path) |
1.5–1.7× | Wired in, opt-in (Models) |
| GraphQL lexer+parser (Rust port, executor stays Python — resolvers are arbitrary user code) | 2.7–3.9× | Wired in automatically (GraphQL) |
Two more wins, found via a real cross-framework comparison#
Building the same sample e-commerce API in Hayavega and two other popular frameworks (one async, one sync) and benchmarking all three (not a synthetic microbenchmark) surfaced two real framework-level fixes:
- Lazy session sweep:
authenticate_session()ran a session- cleanupDELETE+ commit on every authenticated request, not just a read. Gated to run at most once a minute per database instead (DELETEis housekeeping, not a correctness dependency — theSELECT's ownexpires_at > ?filter already rejects an expired session regardless). A real authenticated endpoint that had been losing to the equivalent endpoints in the other two frameworks despite Hayavega winning every other scenario went from behind to a decisive ~2.7–2.9× win. See Authentication. - Opt-in SQLite WAL mode: SQLite's untouched defaults
(
journal_mode=DELETE,synchronous=FULL) cost ~6.4–6.9ms per write-transaction commit on this project's own Windows environment, almost entirelyfsyncoverhead.db.enable_wal_mode()(SQLite's own recommended pairing for write-heavy workloads) drops that to ~0.15–0.17ms — a ~39–47× improvement, confirmed at the HTTP level too (a write-heavy endpoint moved from a near-tie with the async comparison framework to a 2.0–2.4× win). See Models.
Losses, disclosed rather than smoothed over#
This project's standing rule: publish what loses, not only what wins.
- HTTPS overhead: ~0–12% depending on scenario, since the TLS
handshake is paid once per connection and amortized by keep-alive, not
paid per request —
db_list(~12%) was the one scenario where this read as real rather than noise. - Native Rust functions that stay unused: three PyO3 functions
(
serialize_json,compile_selection,normalize_path) were benchmarked against their pure-Python equivalents and lost —serialize_jsonlost tomsgspec.json.encodeby ~21–22×. They remain defined but unwired, correctly, since wiring them in would have been a regression. - A general native template compiler: built, benchmarked, and Python's own interpreter won by 1.4–2.1× even after removing decode-caching as a variable. A narrow, hardcoded Rust renderer for one exact template shape does win (16–20× isolated), but shipping it as the default would mean dropping template-language features — a correctness regression disguised as a performance win.
- The WSGI
template_rendergap: historically documented around ~11% behind the async comparison framework; re-measured across four fresh runs and found to average to a statistical tie, with individual runs swinging up to 17% in either direction — a lesson in not trusting a single-run benchmark number for this specific scenario. json_echoandstatic_fileagainst Raw ASGI, Granian, and Robyn: up to a decisive 3.28× loss to Robyn onjson_echo, and 3.08–3.40× losses to Raw ASGI/Granian onstatic_file-- the two scenarios with the least work per request, against three targets with less framework/adapter overhead per request than Hayavega has on any transport. See "Windows (dev machine), eight-way comparison" above for the full table and the one scenario (db_list) where Hayavega beats all three instead.
Methodology notes worth knowing#
- Windows-specific findings, disclosed as such. This project's own
dev environment is Windows; two real methodology bugs were found and
fixed along the way (a benchmarked server blocking once its
stdoutpipe filled under load; one comparison framework's response objects not settingContent-Length, silently exhausting Windows' ephemeral port range). Both are documented in full inbenchmarks/README.mdso the next framework added to the suite doesn't rediscover them the hard way. - An "environment ceiling" was found and controlled for. A
completely minimal, framework-free WSGI app doing the identical
sqlite3.connect()+SELECThit the same ~700–800 req/s ceiling as every framework in the suite — proving that ceiling is this machine's SQLite-under-concurrency behavior, not something any framework's code can be blamed for or beat through code changes alone.
Full numbers, every benchmark script, and the complete narrative of what
was tried (including the dead ends) live in benchmarks/README.md in
the repository.