Skip to content

Commit 53919b2

Browse files
authored
Merge pull request #7 from fastapi-startkit/fastapi-starkit
README: fastapi vs fastapi-startkit comparison
2 parents ba84092 + 3b2ad70 commit 53919b2

1 file changed

Lines changed: 121 additions & 1 deletion

File tree

README.md

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,4 +628,124 @@ func getUser(ctx *breeze.Context) {
628628
"name": "Alice",
629629
})
630630
}
631-
```
631+
```
632+
633+
---
634+
635+
## FastAPI vs FastAPI-StartKit (Python benchmark)
636+
637+
Two Python ASGI entries in this harness implement the **same three benchmark
638+
routes** with identical handlers (`GET /` → empty, `GET /user/{id}` → the id as
639+
plain text, `POST /user` → empty). This section compares them.
640+
641+
### Overview
642+
643+
| | **fastapi** | **fastapi-startkit** |
644+
|---|---|---|
645+
| What it is | The raw [FastAPI](https://fastapi.tiangolo.com) micro-framework (Starlette + Pydantic). | A batteries-included application framework built **on top of** FastAPI (Laravel-style providers / config / service container). |
646+
| Benchmark app | A single `FastAPI()` instance in `server.py` with three route handlers. | An `Application` booted with a `FastAPIProvider`; the same three routes are registered through the framework's router (`providers/`, `config/`, `bootstrap/`). |
647+
| Directory | `python/fastapi` | `python/fastapi_startkit` |
648+
649+
Both wrap the same underlying FastAPI / Starlette request path — `fastapi-startkit`
650+
adds a thin framework layer around it.
651+
652+
### Correctness
653+
654+
Both pass the shared route spec (`.spec/route_spec.rb`) **6/6**, verified through
655+
the standard Docker + rspec harness and confirmed in-container
656+
(`docker exec … curl localhost:3000`, so it is immune to host-networking
657+
artifacts):
658+
659+
| Route | Expected | fastapi | fastapi-startkit |
660+
|---|---|---|---|
661+
| `GET /` | `200`, empty body |||
662+
| `GET /user/0` | `200`, body `0` |||
663+
| `POST /user` | `200`, empty body |||
664+
665+
### Setup / dependencies
666+
667+
| | **fastapi** | **fastapi-startkit** |
668+
|---|---|---|
669+
| Declared dependency | `fastapi>=0.139,<0.140` | `fastapi-startkit[fastapi]==0.47.0` |
670+
| Underlying FastAPI | `0.139.0` | `0.139.0` (transitive, via `fastapi-startkit==0.47.0`) |
671+
| App composition | single `FastAPI()` in `server.py` | `Application` + `FastAPIProvider` (providers / config / bootstrap) |
672+
| Engines | uvicorn (default), hypercorn, daphne, granian | uvicorn (default), hypercorn, daphne, granian |
673+
| Default build | `.Dockerfile.uvicorn` | `.Dockerfile.uvicorn` |
674+
| Python | 3.14 | 3.14 |
675+
676+
The `fastapi-startkit` dependency is pinned to `==0.47.0`, which resolves the
677+
**same FastAPI 0.139.0 / Starlette 1.3.1** the baseline uses, so both Python
678+
entries benchmark on an identical FastAPI/Starlette stack; both are built and run
679+
from the same default `.Dockerfile.uvicorn` engine.
680+
681+
### Performance
682+
683+
Both serve identical handlers over the **same ASGI server (uvicorn)** in the
684+
**same container recipe**, so the per-request hot path is the same FastAPI /
685+
Starlette routing; `fastapi-startkit` adds framework wiring at **boot** (provider
686+
/ router / container setup), not on the per-request path.
687+
688+
A controlled, same-host relative comparison (from the Benchmark Runner) shows the
689+
two are **close on the GET routes** (within a few percent) with a **larger
690+
framework overhead on `POST /user`** at higher concurrency.
691+
692+
**Relative, same-host comparison — NOT official benchmark figures.**
693+
694+
Method (verbatim):
695+
696+
> macOS + OrbStack; Docker python:3.14-slim; uvicorn --workers=nproc (11
697+
> workers); load = oha 1.14, keepalive/connection-reuse, latency-correction;
698+
> 8s × 4 reps averaged; concurrency 64 and 256; 100% success every cell. Matched
699+
> stack both sides: Python 3.14.6, fastapi 0.139.0, starlette 1.3.1
700+
> (fastapi_startkit built from fastapi-startkit 0.47.0).
701+
702+
Relative delta (fastapi-startkit vs fastapi; negative = fastapi-startkit slower):
703+
704+
| Route | Δ @ c64 | Δ @ c256 |
705+
|---|---|---|
706+
| `GET /` | −2.9% | −6.4% |
707+
| `GET /user/0` | −1.5% | −6.2% |
708+
| `POST /user` | −17.3% | −18.8% |
709+
710+
Absolute throughput (host-specific, for context only): `fastapi` ≈ 41–47k rps,
711+
`fastapi-startkit` ≈ 33–40k rps, 100% success across all cells.
712+
713+
Disclaimer (verbatim):
714+
715+
> Relative, same-host comparison (macOS/OrbStack), NOT official benchmark
716+
> figures. Load uses keepalive, not the harness's --disable-keepalive;
717+
> authoritative numbers require run.sh on Linux. Requires fastapi-startkit 0.47
718+
> so both run FastAPI 0.139.
719+
720+
> **Both sides match the committed build.** The committed benchmark pins
721+
> **`fastapi-startkit==0.47.0`**, which resolves the same **FastAPI 0.139.0 /
722+
> Starlette 1.3.1** the baseline uses. These deltas therefore reflect the
723+
> committed, apples-to-apples build — not a version-skewed comparison.
724+
>
725+
> **Why `POST /user` is slower.** It is *not* `fastapi-startkit` per-request
726+
> code — the hot path is stock FastAPI / Starlette. The gap comes from
727+
> **FastAPI 0.139's `include_router`**: unlike FastAPI ≤ 0.124 (which flattened
728+
> included routes into the app router), 0.139 keeps a nested
729+
> `fastapi.routing._IncludedRouter` node on `app.router.routes`, adding a
730+
> per-request resolution layer. This is **intended FastAPI behavior** (cached
731+
> candidate resolution / router-identity), and it is **reproducible in plain
732+
> FastAPI** — any app that registers routes via `include_router` on 0.139 pays
733+
> it. `fastapi-startkit` uses `include_router` as its idiomatic route-module
734+
> registration, so the benchmark shows this representative cost rather than
735+
> hiding it (e.g. via flat `add_api_route`).
736+
>
737+
> `include_router`'s overhead is a long-standing, acknowledged FastAPI topic.
738+
> A related upstream discussion,
739+
> [fastapi/fastapi#5343](https://github.com/fastapi/fastapi/issues/5343),
740+
> reports `include_router` performance cost and requests a top-level
741+
> `FastAPI(router=…)` bypass — though that report concerns `include_router`'s
742+
> *construction-time* cost (per-route introspection in the app-factory pattern),
743+
> a different mechanism from the *per-request* `_IncludedRouter` resolution
744+
> measured here. Both reflect the same theme: `include_router` carries overhead
745+
> that flat registration avoids.
746+
747+
> ⚠️ **Benchmarking caveat.** Reliable *absolute* numbers under the harness's
748+
> standard `--disable-keepalive` were not obtainable on macOS (ephemeral-port /
749+
> `TIME_WAIT` exhaustion), so the figures above use keepalive and are host-
750+
> specific. Authoritative absolute throughput/latency requires the Linux
751+
> harness (`run.sh`).

0 commit comments

Comments
 (0)