Benchmarks — Hummingbird gegen FastAPI und Fastify
Sechs Artikel lang haben wir das Gateway gebaut, ohne uns um Zahlen zu kümmern. Im letzten Artikel haben wir es deployed; jetzt schauen wir uns an, was es leistet. Wir messen das Hummingbird-Gateway aus Artikel 6 gegen zwei funktional äquivalente Implementierungen: eine in FastAPI mit uvicorn, eine in Fastify 5. Dieselben zwei Endpoints, dieselbe Auth-Logik, derselbe Proxy zum Backend, derselber Middleware-Stack. Einziger Unterschied: das Framework darunter.
Was wir messen und was nicht
Ein LLM-Gateway lebt von zwei sehr unterschiedlichen Betriebsmodi. Erstens einem leichten Pfad: /healthz, /v1/models, Auth-Fehler — das sind kurze, synchrone Responses ohne Backend-Call. Zweitens dem schweren Pfad: /v1/messages mit echtem Backend, wo die Inferenz-Zeit alles dominiert. Beide messen wir, und sie erzählen verschiedene Geschichten.
Was wir nicht messen: End-to-End mit einem echten Modell. Das wäre kein Framework-Benchmark, sondern ein Modell-Benchmark. Stattdessen nutzen wir einen deterministischen Stub, der die Backend-Latenz kontrolliert skaliert — dazu gleich mehr.
Auch nicht gemessen: Streaming-Durchsatz. SSE ist Event-driven; RPS-Zahlen für einen Streaming-Endpoint wären irreführend. Zum qualitativen Streaming-Verhalten gibt es einen eigenen Abschnitt.
Hardware und Tools: M4 Air, 24 GB Unified Memory. Passive Kühlung bedeutet: unter Dauerlast kann das Gerät in den Throttle-Bereich rutschen. Wir arbeiten deshalb mit 30-Sekunden-Bursts statt Dauerläufen. Lastgenerator ist oha 1.14, 50 parallele Connections. Alle Zahlen: Eigenmessung 2026-05-20.
Das Stub-Backend
Echte Inferenz ist für einen Framework-Vergleich ungeeignet — zu viel Varianz, zu schwer zu kontrollieren. Wir ersetzen mlx_lm.server durch einen Python-Stub, der die OpenAI-API nachahmt und die Latenz deterministisch skaliert: zehn Millisekunden pro hundert geschätzte Input-Tokens (chars / 4). Ein Request mit einer 200-Zeichen-Nachricht (~50 Tokens) schläft also 5 ms, bevor er antwortet. Mit 2000 Zeichen (~500 Tokens) sind es 50 ms, mit 8000 Zeichen (~2000 Tokens) 200 ms.
Das gibt uns drei saubere Mess-Regime:
| Payload-Größe | Geschätzte Tokens | Stub-Delay | Charakter |
|---|---|---|---|
| small | ~50 | ~5 ms | Framework dominiert noch |
| medium | ~500 | ~50 ms | Gemischt |
| large | ~2000 | ~200 ms | Backend dominiert |
Der erste Lauf war unehrlich — und warum
Wer dasselbe Gateway in drei Sprachen schreibt und dann benchmarkt, muss auf Vergleichbarkeit achten. Das Gateway aus Artikel 6 läuft pro Request durch vier Middlewares: LogRequestsMiddleware, MetricsMiddleware, TracingMiddleware und GatewayErrorMiddleware. Eine naive Vergleichsimplementierung in FastAPI oder Fastify hätte diese Layer nicht gehabt — und damit weniger Pro-Request-Overhead.
Ein erster Bench-Lauf mit dieser Asymmetrie zeigte Hummingbird bei /healthz mit rund 40k RPS, Fastify mit 64k. Das sah dramatisch aus und war trotzdem irreführend: wir verglichen Production-Stack gegen Barebones. Um das zu korrigieren, bekamen FastAPI und Fastify dieselben vier Layer.
FastAPI bekommt einen ASGI-Middleware-Stack mit opentelemetry.trace.NoOpTracer, prometheus_client-Counter und -Histogram und einen try/except-Error-Handler. Fastify bekommt onRequest/onResponse-Hooks mit @opentelemetry/api, prom-client und einen setErrorHandler. Counter und Histogram tragen dieselben Namen und Labels wie in Hummingbird (hb_requests, http_server_request_duration). Das NoOp-Tracing kostet etwas, tut aber nichts — genau wie in Hummingbird, wo kein OTel-Collector konfiguriert ist.
Alle drei Gateways exponieren /metrics mit dem Prometheus-Textformat. Der Bench läuft Requests durch Auth, Metrics, Tracing und Error-Handler — bei jedem der drei Frameworks.
Reiner Framework-Overhead — der healthz-Pfad
GET /healthz hat kein Backend, keine DB, keine Berechnung. Die Antwort ist ein statisches JSON-Objekt. Was hier gemessen wird, ist nah am puren Framework-Routing-Overhead plus Middleware-Chain.
Messung 2026-05-20, M4 Air 24 GB, oha -z 30s -c 50:
| Gateway | RPS | P50 | P95 | P99 |
|---|---|---|---|---|
| Hummingbird (1 Prozess) | 67 948 | 0.2 ms | 2.9 ms | 4.0 ms |
| FastAPI (4 Worker) | 66 286 | 0.2 ms | 2.7 ms | 3.8 ms |
| Fastify (1 Prozess) | 61 277 | 0.3 ms | 2.5 ms | 3.5 ms |
Hummingbird liegt knapp vor FastAPI — mit einem entscheidenden Unterschied: FastAPI läuft mit vier Worker-Prozessen, Hummingbird mit einem. Pro Prozess gerechnet kommt FastAPI auf etwa 16k RPS; Hummingbird auf 68k. Das ist kein kleiner Unterschied. Fastify läuft ebenfalls als Single-Process und landet bei 61k.
Die Aussage ist nicht „Swift ist fünfmal schneller als Python". Die Aussage ist: SwiftNIO lässt einen einzelnen Prozess alle CPU-Kerne über sein Event-Loop-Pinning ausnutzen, während CPython wegen des GIL mehrere Prozesse braucht. Das führt direkt zum nächsten Punkt.
Mit Backend-Latenz — wo das Framework-Delta verschwindet
Sobald ein echter Backend-Call in der Bahn ist, verändert sich das Bild grundlegend.
Anthropic/small (~5 ms Backend-Delay), Eigenmessung 2026-05-20:
| Gateway | RPS | P50 | P95 | P99 |
|---|---|---|---|---|
| Hummingbird | 883 | 56.2 ms | 65.0 ms | 69.6 ms |
| FastAPI | 903 | 54.6 ms | 62.8 ms | 66.9 ms |
| Fastify | 877 | 56.1 ms | 64.3 ms | 70.5 ms |
Die Abstände sind jetzt im Messrauschen — alle drei liegen innerhalb von 3% RPS und 5 ms P50. Bei 5 ms Backend-Delay ist das Framework-Overhead von unter 1 ms schon fast irrelevant.
Mit 50 ms und 200 ms Backend-Delay wird es noch deutlicher:
Anthropic/medium (~50 ms), Eigenmessung 2026-05-20:
| Gateway | RPS | P50 |
|---|---|---|
| Hummingbird | 111 | 439.7 ms |
| FastAPI | 114 | 427.3 ms |
| Fastify | 114 | 428.2 ms |
Anthropic/large (~200 ms), Eigenmessung 2026-05-20:
| Gateway | RPS | P50 |
|---|---|---|
| Hummingbird | 30.9 | 1633.0 ms |
| FastAPI | 30.9 | 1655.8 ms |
| Fastify | 30.9 | 1652.4 ms |
Bei 200 ms Backend-Latenz sind alle drei Gateways auf einem Feld. Der P50 liegt bei 1630-1650 ms — das sind fast ausschließlich die 50 parallelen Connections in der Queue vor dem Stub-Backend, nicht Framework-Overhead. Mit einem echten Modell, das pro Token 10-50 ms braucht, wäre der Effekt noch ausgeprägter.
Die Punchline: für einen LLM-Gateway ist die Framework-Wahl für End-to-End-Latenz und Throughput irrelevant. Das Modell dominiert.
Memory und Startup — hier entscheiden Architektur-Entscheidungen
Das Bild ändert sich, wenn wir nicht RPS messen, sondern Betriebskosten: wie viel Memory braucht der Prozess idle, und wie lange dauert der Cold-Start?
Eigenmessung 2026-05-20, M4 Air, nach dem Warmup (alle Worker geladen):
| Gateway | Startup | Idle RSS gesamt | Prozesse |
|---|---|---|---|
| Hummingbird | 44 ms | 16 MiB | 1 |
| FastAPI | 472 ms | 349 MiB | 6 (master + watcher + 4 worker) |
| Fastify | 211 ms | 75 MiB | 1 |
Der FastAPI-Wert von 349 MiB ist die Summe aller sechs Prozesse. Ein einzelner uvicorn-Worker bringt rund 75 MiB mit — fünf davon plus Master und Watcher ergeben 349 MiB. Fastify kommt als einzelner Node.js-Prozess auf 75 MiB. Hummingbird läuft als einzelner Prozess mit 16 MiB.
Das ist eine strukturelle Eigenschaft, kein Tuning-Problem. Python importiert beim Start das gesamte FastAPI/Pydantic/httpx/prometheus_client-Ökosystem. Node.js lädt V8 und die npm-Abhängigkeiten. SwiftNIO kommt als statisch gelinktes Framework im Binary mit; Laufzeit-Imports gibt es nicht.
Für ein Deployment auf einem einzelnen Homeserver macht das keinen Unterschied. Für den Betrieb vieler Gateway-Instanzen auf einem Knoten, für Edge-Deployments mit wenig RAM oder für Function-as-a-Service-Umgebungen ist es ein echter Faktor.
Deployment-Artefakt
Ein letzter Größenvergleich — was landet tatsächlich auf dem Zielhost?
| Gateway | Artefakt | Größe |
|---|---|---|
| Hummingbird | Static Linux SDK Binary | 18 MB |
| FastAPI | Source-Code | 28 KB + Python-Laufzeit + venv |
| Fastify | dist/server.js + node_modules | 8 KB + 54 MB |
Das Hummingbird-Binary aus dem Swift Static Linux SDK ist komplett selbst-enthalten — kein Python, kein Node.js, keine Laufzeit-Abhängigkeiten. Für Fastify gilt das Gegenteil: der kompilierte Code ist winzig, aber die node_modules-Bäume wachsen mit jeder Dependency. Das spiegelt sich direkt in Container-Image-Größen.
Streaming — qualitativ, ohne Zahlen
RPS-Metriken für SSE-Endpoints sind irreführend: Streaming-Responses bleiben Sekunden bis Minuten offen, sodass Throughput-Messungen im klassischen Sinn keinen Sinn ergeben. Stattdessen drei qualitative Punkte, die im Code direkt ablesbar sind:
Cancellation bei Client-Disconnect. Wenn der Client die Verbindung trennt, muss der Server den Backend-Request abbrechen. In Hummingbird propagiert das über AsyncStream.onTermination → Task.cancel() → URLSession wird abgebrochen — ohne extra Code, weil Swift Structured Concurrency das automatisch durchreicht. In Fastify muss man reply.raw.on('close', ...) explizit verdrahten; es passiert nicht von selbst. In FastAPI ist Request.is_disconnected() eine Polling-API, die man in die Generator-Funktion einbauen muss.
Backpressure. Was passiert, wenn der Client langsamer liest als das Backend liefert? SwiftNIO hat Backpressure baked-in — Schreibpuffer stoppt wenn der Client nicht liest, der Backend-Task wird gebremst. Node.js-Streams haben highWaterMark, das man konfigurieren muss. FastAPI’s StreamingResponse hat keinen eingebauten Backpressure-Mechanismus; bei einem langsamen Client wächst der Buffer im Worker-Memory.
Concurrent Streams. Alle drei können hohe SSE-Concurrency. Hummingbird und Fastify skalieren über den Event-Loop; FastAPI-Streaming läuft über async Generators in einzelnen Worker-Prozessen, was bei vielen parallelen Streams mehrere Worker besonders wichtig macht.
Das sind Beobachtungen aus dem Code — nicht gemessen, aber im Design sichtbar.
Wann lohnt sich welche Wahl
Hummingbird / Swift lohnt sich strukturell, wenn:
- Memory-Footprint auf dem Zielhost ein Faktor ist (Edge, viele Instanzen)
- Cold-Start-Latenz wichtig ist (FaaS, Kubernetes-Scale-From-Zero)
- Deployment-Artefakt-Größe zählt (Air-Gapped, Container-Registry-Traffic)
- Das Team Swift schreibt oder iOS-Erfahrung mitbringt, die auf Server-Code übertragbar ist
FastAPI oder Fastify lohnen sich, wenn:
- Das Team Python oder JavaScript/TypeScript kann
- Das Ökosystem wichtig ist (Tooling, Libraries, Community)
- Throughput und Latenz der Gateway-Schicht kein Differentiator sind — und das sind sie es bei einem LLM-Gateway fast nie
Was kein Argument ist: „FastAPI ist langsam" oder „Hummingbird ist dreimal schneller". Pro Prozess ist Hummingbird substanziell effizienter. Unter Production-Last mit echtem Modell sieht der Client davon nichts. Das ist kein Widerspruch; es sind zwei verschiedene Metriken.
Reproduktion
Der gesamte Bench-Code liegt in bench/ im Gateway-Repo. Mit make setup werden die Python-Dependencies und npm-Packages installiert, mit make all laufen alle drei Gateways durch alle vier Szenarien und results/summary.txt wird geschrieben. Die Zahlen in diesem Artikel stammen aus dem Lauf mit oha 1.14 auf dem oben beschriebenen M4 Air; auf einem anderen System — mit mehr Kernen, mit aktivem Cooling oder unter anderer Last — werden die absoluten Zahlen abweichen. Die relativen Verhältnisse sollten stabil bleiben.