Skip to content

Performance

jero is built for speed, but the only honest way to talk about speed is with numbers and a clear account of how they were produced. This page is that account.

The short version: across four workloads benchmarked side by side against seven other frameworks — Python (Litestar, FastAPI, Blacksheep, Robyn, Flask), Go (Gin), and Bun (Elysia) — jero led the Python frameworks tested in every scenario. On the pure framework hot path (a typed JSON GET) it topped this benchmark table by overall score, ahead of both the Go and the Bun service. On the I/O-bound scenarios (an upstream proxy, a database read) Go pulled well clear — there the bottleneck is the HTTP-client and database-driver ecosystem, not the framework, and that's a fight Python doesn't win today.

Read the caveats. These are favourable, constrained conditions, and a microbenchmark is not your application.

And yes — we know benchmarks are genuinely hard to do right and to do fairly. Every framework has a configuration that flatters it, every harness makes choices that nudge the numbers, and reasonable people disagree about what "fair" even means. This is one benchmark, run one way, on one machine. We've tried to be even-handed and we show exactly how it was produced below so you can judge for yourself — but please treat it as a single data point, not the last word. If you have a workload that matters to you, the only number worth trusting is the one you measure yourself.

How the numbers were produced

The benchmark runs each framework in isolation, one at a time. Only one framework server is up at any moment, alongside its own freshly-started dependencies — a Rust upstream service (for the proxy scenario) and a fresh Postgres (for the database scenario). Nothing else competes for the machine. This removes cross-framework contention and shared-state effects, so each number reflects that framework alone.

  • Load generator: k6, a fixed virtual-user (VU) count hammering the service for a fixed duration.
  • Best-of-N: every (framework, scenario) pair is run N times and the best run is kept. Repeating and taking the best beats down the ~3–4% run-to-run noise floor so the comparison reflects each framework's ceiling, not a noisy sample.
  • Single worker, single core: every framework runs with one worker process; Go is pinned to GOMAXPROCS=1. This is a like-for-like, single-core comparison — not a test of how well each scales across cores.
  • Identical scenarios — the same request scripts, the same selection logic, and the same scoring table for every framework.

Run configuration

Setting Value
Machine Apple M3 Max, 36 GB
Concurrency 100 VUs
Duration 30s per run
Best-of-N 3 runs
Workers 1 (Go pinned to GOMAXPROCS=1)
Python server Granian, single worker

Results

req/s is throughput (higher is better); mean and p99 are request latency (lower is better). vs all is an aggregate score across all three — a single "overall standing" number, normalised so jero = 1.00× in every scenario. Every framework returned 100% successful responses in every run, so that column is omitted. Frameworks are ordered by vs all within each scenario.

1 — GET /info — the pure framework path

Route → build a typed JSON response with a typed response header → encode. No I/O. This isolates routing and serialization, and is the closest thing to a measure of the framework's own per-request overhead.

Framework req/s mean p99 vs all
jero 44.5k 2.22ms 3.73ms 1.00×
blacksheep 40.3k 2.45ms 3.36ms 0.97×
elysia (Bun) 38.7k 2.55ms 3.52ms 0.93×
gin (Go) 38.4k 2.57ms 3.79ms 0.90×
litestar 35.6k 2.78ms 3.99ms 0.84×
fastapi 24.5k 4.06ms 4.81ms 0.62×
robyn 20.6k 4.83ms 10.46ms 0.42×
flask 17.9k 5.56ms 19.29ms 0.31×

2 — POST /movies — the authed write path (JWT)

Bearer/JWT auth → msgspec decode of the request body → handler → encode → 201. The realistic write path for a typed JSON API.

Framework req/s mean p99 vs all
gin (Go) 28.6k 3.46ms 6.39ms 1.06×
jero 27.4k 3.62ms 6.93ms 1.00×
elysia (Bun) 24.0k 4.12ms 8.20ms 0.87×
blacksheep 16.4k 6.05ms 14.58ms 0.55×
robyn 15.7k 6.21ms 18.04ms 0.50×
litestar 12.0k 8.25ms 22.52ms 0.39×
flask 10.5k 9.46ms 48.39ms 0.28×
fastapi 5.2k 18.97ms 55.64ms 0.17×

jero lands within ~5% of a hand-written Go service here, and led the Python frameworks tested by a wide margin.

3 — GET proxy — bound by the HTTP client

The service makes an outbound HTTP call to the Rust upstream and relays the response. The bottleneck is the HTTP client library, not the framework — which is why the whole Python field clusters together and Go runs away.

Framework req/s mean p99 vs all
gin (Go) 15.1k 6.58ms 15.34ms 5.35×
elysia (Bun) 11.2k 8.77ms 21.11ms 3.96×
jero 3.2k 31.56ms 102.24ms 1.00×
litestar 2.8k 35.17ms 127.50ms 0.86×
blacksheep 2.9k 33.85ms 158.69ms 0.82×
fastapi 2.4k 42.21ms 102.92ms 0.82×
robyn 2.5k 40.37ms 167.62ms 0.72×
flask 2.4k 41.94ms 166.82ms 0.70×

jero led the Python frameworks tested, but Go's mature native HTTP stack is in a different class. This gap is the ecosystem, not jero.

4 — GET /users/me — bound by the database driver

Reads a row from Postgres. The bottleneck is the database driver, so again the field compresses and Go's native driver leads.

Framework req/s mean p99 vs all
gin (Go) 16.2k 6.13ms 8.72ms 2.44×
elysia (Bun) 6.0k 16.45ms 16.77ms 1.02×
jero 8.4k 11.84ms 33.98ms 1.00×
blacksheep 7.8k 12.84ms 89.58ms 0.69×
litestar 6.3k 15.77ms 141.70ms 0.51×
robyn 4.6k 21.87ms 172.95ms 0.39×
fastapi 3.4k 29.26ms 104.84ms 0.38×
flask 1.3k 78.03ms 210.18ms 0.15×

jero led the Python frameworks tested again. Go was well ahead; Bun's lower p99 edged jero on aggregate score despite lower throughput and higher mean latency.

How to read this

  • jero leads the Python frameworks tested in all four scenarios. That is the durable claim.
  • On the pure framework path it beats even Go and Bun. That result is real but narrow: an in-memory JSON path plays directly to Python + msgspec's strengths and to the Rust HTTP layer underneath. It is not evidence that Python is faster than Go in general — and we are not making that claim.
  • On I/O-bound paths, Go is well ahead. When the work is an outbound HTTP call or a database query, the framework is barely in the picture; the HTTP client and database driver decide it, and Go's native libraries dominate. jero stays ahead of the Python frameworks tested, which is the most it can do there.
  • A benchmark is not your app. Single worker, single core, localhost, fixed payloads, best-of-N. Real workloads have more moving parts. Treat these as directional evidence that jero's per-request overhead is low — not as a promise about your production numbers.

Where jero's design earns these numbers: all type introspection happens once, at startup. The request path is dict lookup → msgspec decode → handler call → encode, and nothing is ever added to it. See the design philosophy for why that's a deliberate, non-negotiable bet.