Benchmarks
One core of an Apple M3 Pro, Python 3.13, 64 keep-alive connections, median of three 5-second runs per rung. Every number comes from a JSON file under bench/baseline/ in the repository; this page is generated from those files.
Where things stand
Microseconds per request and requests per second, one core; lower microseconds is better. The first column is the day-one ladder run of FastAPI 0.141 on uvicorn, the other two are the current tree.
| Route | FastAPI 0.141 day one, uvicorn | notslowapi, uvicorn | notslowapi, granian |
|---|---|---|---|
| raw ASGI app | 13.7 µs73,040 req/s | 13.5 µs74,172 req/s | 7.8 µs128,801 req/s |
| Starlette route | 19.5 µs51,183 req/s | 17.4 µs57,533 req/s | 8.5 µs117,204 req/s |
| Starlette, int path + str query | not measured | 20.9 µs47,773 req/s | not measured |
| notslowapi, typed dict return | 31.2 µs32,033 req/s | 18.3 µs54,793 req/s | 9.1 µs110,154 req/s |
| notslowapi, untyped dict return | not measured | 21.2 µs47,257 req/s | not measured |
| notslowapi, route via include_router | not measured | 21.2 µs47,173 req/s | not measured |
| Starlette, 50 routes | not measured | 20.4 µs49,117 req/s | not measured |
| notslowapi, 50 routes | not measured | 21.9 µs45,746 req/s | not measured |
| notslowapi, 50 routes via include_router | not measured | 26.3 µs37,968 req/s | 17.3 µs57,864 req/s |
| notslowapi, int path + str query | 57.0 µs17,531 req/s | 24.2 µs41,405 req/s | 14.3 µs69,742 req/s |
| notslowapi, pydantic body + response_model | 52.1 µs19,212 req/s | 26.1 µs38,295 req/s | 20.1 µs49,777 req/s |
Rungs added after day one show as not measured in the first column. Single-core numbers; multi-core scaling needs a separate client machine. Source files: results_ladder_v1.json and results_ladder_v3.json.
Every change, measured before and after
Each row is one committed change, measured in the same session with the previous tree checked out for the before run. Rungs not touched by a change are controls. A dot marks a rung that got faster.
| change | rung | before µs | after µs | delta |
|---|---|---|---|---|
| fix1 | l1b_starlette_params | 25.3 | 22.4 | -12% |
| fix1 | l2_fastapi_dict | 31.2 | 30.7 | -2% |
| fix1 | l3_fastapi_params | 50.7 | 35.3 | -30% |
| fix2 | l2_fastapi_dict | 31.0 | 26.3 | -15% |
| fix2 | l3_fastapi_params | 36.2 | 32.6 | -10% |
| fix2 | l4_fastapi_model | 42.7 | 36.7 | -14% |
| fix5 | l2_fastapi_dict | 25.7 | 24.6 | -4% |
| fix5 | l2c_fastapi_included | 29.2 | 27.2 | -7% |
| fix5 | l3_fastapi_params | 31.6 | 29.7 | -6% |
| fix5 | l4_fastapi_model | 36.5 | 32.3 | -11% |
| fix6 | l1_starlette | 19.5 | 19.4 | -0% |
| fix6 | l1b_starlette_params | 21.5 | 22.3 | +4% |
| fix6 | l2_fastapi_dict | 24.6 | 23.8 | -3% |
| fix7 | l4_fastapi_model | 32.3 | 31.5 | -3% |
| fix7 | l2_fastapi_dict | 23.8 | 22.7 | -4% |
| fix8 | l1_starlette | 19.4 | 18.6 | -4% |
| fix8 | l1b_starlette_params | 22.3 | 21.6 | -3% |
| fix8 | l2_fastapi_dict | 22.7 | 22.7 | -0% |
| fix9 | l3_fastapi_params | 29.7 | 27.5 | -7% |
| fix9 | l4_fastapi_model | 31.5 | 30.4 | -4% |
| fix9 | l2_fastapi_dict | 22.7 | 22.0 | -3% |
| fix10 | l1_starlette | 17.9 | 18.3 | +2% |
| fix10 | l2_fastapi_dict | 21.8 | 21.3 | -2% |
| fix10 | l1c_starlette_50routes | 26.7 | 20.5 | -23% |
| fix10 | l5_fastapi_50routes | 36.3 | 25.2 | -31% |
| fix11 | l2c_fastapi_included | 28.7 | 26.8 | -7% |
| fix11 | l5_fastapi_50routes | 31.9 | 25.1 | -21% |
| fix11 | l5b_fastapi_50routes_included | 92.2 | 31.7 | -66% |
| fix12 | l2c_fastapi_included | 26.8 | 23.1 | -14% |
| fix12 | l5_fastapi_50routes | 25.1 | 25.1 | -0% |
| fix12 | l5b_fastapi_50routes_included | 31.7 | 29.8 | -6% |
| fix13 | l1_starlette | 18.6 | 18.0 | -3% |
| fix13 | l1b_starlette_params | 21.7 | 21.0 | -3% |
| fix13 | l2_fastapi_dict | 21.6 | 21.0 | -3% |
| fix14 | l2_fastapi_dict | 21.6 | 20.8 | -4% |
| fix14 | l4_fastapi_model | 30.6 | 28.0 | -8% |
| fix14 | l4_fastapi_model[granian] | 28.4 | 25.1 | -12% |
| fix15_16 | l1_starlette | 17.5 | 17.0 | -3% |
| fix15_16 | l2_fastapi_dict | 20.5 | 18.4 | -10% |
| fix15_16 | l3_fastapi_params | 26.1 | 25.3 | -3% |
| fix15_16 | l2_fastapi_dict[granian] | 10.7 | 9.2 | -13% |
| fix15 | l2_fastapi_dict | 20.5 | 22.6 | +11% |
| fix15 | l3_fastapi_params | 26.6 | 24.7 | -7% |
| fix15 | l2_fastapi_dict[granian] | 14.0 | 10.6 | -24% |
| fix17 | l2_fastapi_dict | 18.9 | 18.2 | -3% |
| fix17 | l3_fastapi_params | 25.4 | 24.9 | -2% |
| fix17 | l4_fastapi_model | 27.2 | 26.8 | -2% |
| fix17 | l2_fastapi_dict[granian] | 9.4 | 9.9 | +6% |
| fix18 | l2_fastapi_dict | 19.4 | 18.9 | -2% |
| fix18 | l3_fastapi_params | 26.1 | 25.1 | -4% |
| fix18 | l2_fastapi_dict[granian] | 10.5 | 12.6 | +20% |
| fix19 | l2_fastapi_dict | 18.4 | 18.0 | -2% |
| fix19 | l3_fastapi_params | 24.8 | 24.6 | -1% |
| fix19 | l2_fastapi_dict[granian] | 10.0 | 10.0 | +0% |
| fix20 | l1_starlette | 17.2 | 16.9 | -2% |
| fix20 | l2_fastapi_dict | 17.8 | 17.1 | -4% |
| fix20 | l5_fastapi_50routes | 21.8 | 21.6 | -1% |
| fix20 | l2_fastapi_dict[granian] | 8.7 | 8.9 | +2% |
| fix21 | l1_starlette | 17.5 | 16.8 | -4% |
| fix21 | l2b_fastapi_untyped | 20.7 | 20.6 | -1% |
| fix21 | l1_starlette[granian] | 10.2 | 9.4 | -9% |
| fix22 | l2_fastapi_dict | 17.2 | 17.0 | -1% |
| fix22 | l2b_fastapi_untyped | 20.3 | 18.0 | -11% |
| fix23 | l1b_starlette_params | 20.1 | 18.3 | -9% |
| fix23 | l3_fastapi_params | 24.8 | 22.4 | -10% |
| fix23 | l3_fastapi_params[granian] | 14.5 | 12.3 | -15% |
Method
The ladder isolates layers: a raw ASGI app, a Starlette route, then notslowapi routes of increasing complexity, all under the same server flags. Load comes from oha; the native split between interpreter and kernel comes from sample(1); Python-level attribution from pyinstrument started in the ASGI lifespan. Surprising numbers are re-measured in alternating after, before, after runs before they count. Details in the docs.