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.

RouteFastAPI 0.141 day one, uvicornnotslowapi, uvicornnotslowapi, granian
raw ASGI app13.7 µs73,040 req/s13.2 µs75,943 req/s7.9 µs126,946 req/s
Starlette route19.5 µs51,183 req/s15.6 µs63,909 req/s8.3 µs120,782 req/s
Starlette, int path + str querynot measured18.0 µs55,521 req/snot measured
notslowapi, typed dict return31.2 µs32,033 req/s15.7 µs63,757 req/s8.1 µs123,416 req/s
notslowapi, untyped dict returnnot measured15.5 µs64,352 req/snot measured
notslowapi, route via include_routernot measured15.4 µs65,132 req/s8.3 µs120,761 req/s
Starlette, 50 routesnot measured15.4 µs65,013 req/snot measured
notslowapi, 50 routesnot measured15.3 µs65,489 req/snot measured
notslowapi, 50 routes via include_routernot measured15.2 µs65,887 req/s8.3 µs120,211 req/s
notslowapi, int path + str query57.0 µs17,531 req/s17.0 µs58,977 req/s8.8 µs113,996 req/s
notslowapi, pydantic body + response_model52.1 µs19,212 req/s18.0 µs55,692 req/s12.4 µs80,445 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.

changerungbefore µsafter µsdelta
fix1l1b_starlette_params25.322.4-12%
fix1l2_fastapi_dict31.230.7-2%
fix1l3_fastapi_params50.735.3-30%
fix2l2_fastapi_dict31.026.3-15%
fix2l3_fastapi_params36.232.6-10%
fix2l4_fastapi_model42.736.7-14%
fix5l2_fastapi_dict25.724.6-4%
fix5l2c_fastapi_included29.227.2-7%
fix5l3_fastapi_params31.629.7-6%
fix5l4_fastapi_model36.532.3-11%
fix6l1_starlette19.519.4-0%
fix6l1b_starlette_params21.522.3+4%
fix6l2_fastapi_dict24.623.8-3%
fix7l4_fastapi_model32.331.5-3%
fix7l2_fastapi_dict23.822.7-4%
fix8l1_starlette19.418.6-4%
fix8l1b_starlette_params22.321.6-3%
fix8l2_fastapi_dict22.722.7-0%
fix9l3_fastapi_params29.727.5-7%
fix9l4_fastapi_model31.530.4-4%
fix9l2_fastapi_dict22.722.0-3%
fix10l1_starlette17.918.3+2%
fix10l2_fastapi_dict21.821.3-2%
fix10l1c_starlette_50routes26.720.5-23%
fix10l5_fastapi_50routes36.325.2-31%
fix11l2c_fastapi_included28.726.8-7%
fix11l5_fastapi_50routes31.925.1-21%
fix11l5b_fastapi_50routes_included92.231.7-66%
fix12l2c_fastapi_included26.823.1-14%
fix12l5_fastapi_50routes25.125.1-0%
fix12l5b_fastapi_50routes_included31.729.8-6%
fix13l1_starlette18.618.0-3%
fix13l1b_starlette_params21.721.0-3%
fix13l2_fastapi_dict21.621.0-3%
fix14l2_fastapi_dict21.620.8-4%
fix14l4_fastapi_model30.628.0-8%
fix14l4_fastapi_model[granian]28.425.1-12%
fix15_16l1_starlette17.517.0-3%
fix15_16l2_fastapi_dict20.518.4-10%
fix15_16l3_fastapi_params26.125.3-3%
fix15_16l2_fastapi_dict[granian]10.79.2-13%
fix15l2_fastapi_dict20.522.6+11%
fix15l3_fastapi_params26.624.7-7%
fix15l2_fastapi_dict[granian]14.010.6-24%
fix17l2_fastapi_dict18.918.2-3%
fix17l3_fastapi_params25.424.9-2%
fix17l4_fastapi_model27.226.8-2%
fix17l2_fastapi_dict[granian]9.49.9+6%
fix18l2_fastapi_dict19.418.9-2%
fix18l3_fastapi_params26.125.1-4%
fix18l2_fastapi_dict[granian]10.512.6+20%
fix19l2_fastapi_dict18.418.0-2%
fix19l3_fastapi_params24.824.6-1%
fix19l2_fastapi_dict[granian]10.010.0+0%
fix20l1_starlette17.216.9-2%
fix20l2_fastapi_dict17.817.1-4%
fix20l5_fastapi_50routes21.821.6-1%
fix20l2_fastapi_dict[granian]8.78.9+2%
fix21l1_starlette17.516.8-4%
fix21l2b_fastapi_untyped20.720.6-1%
fix21l1_starlette[granian]10.29.4-9%
fix22l2_fastapi_dict17.217.0-1%
fix22l2b_fastapi_untyped20.318.0-11%
fix23l1b_starlette_params20.118.3-9%
fix23l3_fastapi_params24.822.4-10%
fix23l3_fastapi_params[granian]14.512.3-15%
fix24l2_fastapi_dict18.318.6+1%
fix24l4_fastapi_model26.722.1-17%
fix24l4_fastapi_model[granian]21.415.7-27%
fix25l1b_starlette_params20.119.2-4%
fix25l2_fastapi_dict19.119.1-0%
fix25l3_fastapi_params23.223.0-1%
fix25l3_fastapi_params[granian]14.813.5-8%
fix26l2_fastapi_dict17.717.5-1%
fix26l2c_fastapi_included21.018.3-13%
fix26l5b_fastapi_50routes_included26.620.6-22%
fix26l5b_fastapi_50routes_included[granian]17.510.7-39%
fix27l2_fastapi_dict17.517.6+1%
fix27l1c_starlette_50routes19.716.9-15%
fix27l5_fastapi_50routes22.118.3-17%
fix27l5b_fastapi_50routes_included26.321.2-19%
fix28l0_raw13.513.5-0%
fix28l2_fastapi_dict17.517.2-2%
fix28l3_fastapi_params22.422.0-2%
fix28l2_fastapi_dict[granian]9.28.9-3%
fix28l3_fastapi_params[granian]11.911.3-5%
fix29l0_raw14.214.3+0%
fix29l2_fastapi_dict17.916.6-7%
fix29l2b_fastapi_untyped18.918.3-3%
fix29l2_fastapi_dict[granian]9.99.1-8%
fix30l0_raw14.714.3-3%
fix30l2_fastapi_dict17.516.6-5%
fix30l3_fastapi_params22.721.7-4%
fix30l3_fastapi_params[granian]13.116.5+27%
fix31l0_raw14.515.5+7%
fix31l2_fastapi_dict16.616.8+1%
fix31l2b_fastapi_untyped18.417.1-7%
fix33l0_raw14.514.4-1%
fix33l2_fastapi_dict16.616.7+0%
fix33l3_fastapi_params21.418.9-12%
fix33l3_fastapi_params[granian]11.410.0-12%
fix34l0_raw13.913.5-3%
fix34l3_fastapi_params18.618.0-3%
fix34l6_fastapi_depends38.128.9-24%
fix34l6_fastapi_depends[granian]29.619.8-33%
fix35l0_raw13.613.5-1%
fix35l6_fastapi_depends29.027.3-6%
fix35l6_fastapi_depends[granian]19.518.1-7%
fix36l0_raw13.613.6+0%
fix36l6_fastapi_depends27.125.8-5%
fix36l6_fastapi_depends[granian]17.716.1-9%
fix37l0_raw13.913.7-1%
fix37l3_fastapi_params19.018.4-3%
fix37l6_fastapi_depends25.925.0-3%
fix37l6_fastapi_depends[granian]16.415.0-9%
fix38l0_raw13.614.0+3%
fix38l2_fastapi_dict15.815.8+0%
fix38l4_fastapi_model21.219.3-9%
fix38l4_fastapi_model[granian]14.613.1-10%
fix39l0_raw13.413.5+0%
fix39l1_starlette16.916.3-4%
fix39l1b_starlette_params18.717.8-5%
fix39l1c_starlette_50routes16.816.3-3%
fix39l1_starlette[granian]8.89.5+7%
fix40l0_raw14.013.7-2%
fix40l6_fastapi_depends25.024.8-1%
fix40l6_fastapi_depends[granian]15.614.7-6%
fix41l0_raw13.413.7+2%
fix41l3_fastapi_params18.317.7-3%
fix41l6_fastapi_depends25.023.3-7%
fix41l3_fastapi_params[granian]9.39.0-2%
fix41l6_fastapi_depends[granian]14.513.0-11%
fix43l6_fastapi_depends23.221.4-8%
fix43l3_fastapi_params[granian]10.310.4+1%
fix43l6_fastapi_depends[granian]13.512.3-9%
fix44l2c_fastapi_included17.617.3-2%
fix44l2_fastapi_dict[granian]9.29.2+1%
fix44l2c_fastapi_included[granian]10.49.3-11%
fix44l5b_fastapi_50routes_included[granian]9.79.3-3%

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.