Benchmarks — Hummingbird gegen FastAPI und Fastify

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ößeGeschätzte TokensStub-DelayCharakter
small~50~5 msFramework dominiert noch
medium~500~50 msGemischt
large~2000~200 msBackend 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:

GatewayRPSP50P95P99
Hummingbird (1 Prozess)67 9480.2 ms2.9 ms4.0 ms
FastAPI (4 Worker)66 2860.2 ms2.7 ms3.8 ms
Fastify (1 Prozess)61 2770.3 ms2.5 ms3.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:

GatewayRPSP50P95P99
Hummingbird88356.2 ms65.0 ms69.6 ms
FastAPI90354.6 ms62.8 ms66.9 ms
Fastify87756.1 ms64.3 ms70.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:

GatewayRPSP50
Hummingbird111439.7 ms
FastAPI114427.3 ms
Fastify114428.2 ms

Anthropic/large (~200 ms), Eigenmessung 2026-05-20:

GatewayRPSP50
Hummingbird30.91633.0 ms
FastAPI30.91655.8 ms
Fastify30.91652.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):

GatewayStartupIdle RSS gesamtProzesse
Hummingbird44 ms16 MiB1
FastAPI472 ms349 MiB6 (master + watcher + 4 worker)
Fastify211 ms75 MiB1

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?

GatewayArtefaktGröße
HummingbirdStatic Linux SDK Binary18 MB
FastAPISource-Code28 KB + Python-Laufzeit + venv
Fastifydist/server.js + node_modules8 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.onTerminationTask.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.