Node.js skalieren, die unspektakuläre Version
Die meisten Artikel zum Thema „Node.js skalieren“ stammen von Leuten, die einen einzigen Service auf eine Million Requests pro Tag skaliert haben und davon ausgehen, dass sich das verallgemeinern lässt. Wir haben mehrere Systeme über 10 Millionen Requests pro Tag hinaus betrieben, und die Lektionen daraus sind alles andere als glamourös.
Das ist das Playbook, das wir uns vor vier Jahren gewünscht hätten.
Schluss mit der Microservices-Debatte
Der teuerste Fehler, den wir bei Teams immer wieder beobachten, ist die Aufteilung in Microservices, bevor es nötig war. Ein Monolith mit sauberen Modulgrenzen skaliert weiter, als die meisten Entwickler erwarten – und ist um eine Größenordnung günstiger im Betrieb.
Unsere Regel: monolithisch bleiben, bis eine der folgenden Bedingungen zutrifft.
- Ein bestimmter Pfad hat ein anderes Skalierungsverhalten als der Rest (z. B. ein rechenintensiver ML-Inferenz-Endpunkt).
- Ein bestimmter Pfad hat einen anderen Deployment-Rhythmus (z. B. Compliance-gebundener Code, der monatlich ausgeliefert werden muss, während der Rest täglich deployt wird).
- Das Team ist über 25 Entwickler groß, und Merge-Konflikte werden zum echten Problem.
Trifft nichts davon zu, bringt die Aufteilung in Services nur operativen Aufwand – Service Discovery, Distributed Tracing, Deployment-Orchestrierung, Netzwerk-Fehlerfälle – ohne einen Vorteil, den ein sauberes Monorepo nicht auch böte.
Der eigentliche Flaschenhals ist die Datenbank
In jedem Node.js-Skalierungsprojekt, das wir umgesetzt haben, ist der Flaschenhals irgendwann zur Datenbank gewandert. CPU und Arbeitsspeicher der Node.js-Services sind meist unproblematisch, weil Node bei I/O-Nebenläufigkeit stark ist. Die eigentlichen Probleme sind:
- Unbegrenzte Connection Pools, die Postgres in die Knie zwingen
- Ein einzelner stark genutzter Index, der zum Schreib-Flaschenhals wird
- N+1-Queries in ORMs, die im Code-Review harmlos aussehen
- Lang laufende Transaktionen, die während der Business-Logik Locks halten
- Lesezugriffe auf der Primärinstanz statt auf einer Read-Replica
Praktische erste Schritte:
- PgBouncer oder ein Äquivalent vor Postgres, im Transaction-Pooling-Modus, mit einem Connection-Limit, das sich an der tatsächlichen Datenbank-CPU orientiert – nicht am Bauchgefühl aus der Node.js-Anwendung.
- Read-Replicas mit expliziter Routing-Logik. Überlassen Sie die Entscheidung nicht dem ORM. Legen Sie zwei Clients im Code an:
db.readunddb.write. Jeder Entwickler muss sich bewusst für einen entscheiden. - Query-Timing in Produktion. Pino plus ein Slow-Query-Logger. Ziel ist ein Slack-Alert, sobald eine Query 200ms überschreitet.
- EXPLAIN ANALYZE in der CI für neue Queries. Wir richten das als PR-Check für alle Änderungen ein, die das Schema berühren.
Caching in drei Schichten
Caching rettet Sie in dieser Reihenfolge: CDN, In-Memory, Redis. Lassen Sie eine Schicht aus, und Sie werden es bereuen.
CDN. Alles, was sich am Edge cachen lässt, sollte auch dort gecacht werden. Das schließt API-Antworten für unauthentifizierte Lesezugriffe ein. Nichts Kompliziertes nötig – Cache-Control-Header und ein CDN, das sie respektiert, lösen 60% des Lesetraffics quasi kostenlos.
In-Memory. Ein kleiner LRU-Cache (wir verwenden lru-cache) vor Redis fängt den Long Tail wiederholter Requests innerhalb eines einzelnen Prozesses ab. Klingt trivial, spart in unseren Profilen aber 30–50% des Redis-Traffics.
Redis. Für gemeinsam genutzten State und berechnete Werte, die über Prozessgrenzen hinweg bestehen bleiben. Setzen Sie es bewusst ein – jeder Key braucht einen Owner, eine TTL und eine dokumentierte Invalidierungsstrategie. Nicht nachverfolgte Redis-Keys sind der Grund, warum Produktions-Caches am Black Friday plötzlich 6 Monate alte Daten ausliefern.
Alles asynchron machen, was asynchron sein kann
Synchrone Request-Pfade sollten das Minimum tun: validieren, persistieren, antworten. Alles andere – E-Mails, Webhook-Aufrufe, Bildverarbeitung, Aufrufe an Drittanbieter-APIs, Suchindexierung – gehört in eine Queue.
Wir setzen für fast alles BullMQ auf Redis ein. Aus zwei Gründen:
- Fehlerisolation. Eine unzuverlässige Drittanbieter-API kann Ihren Request-Pfad nicht lahmlegen, wenn der Aufruf in einem Worker stattfindet.
- Backpressure. Wenn der Traffic sprunghaft ansteigt, fängt die Queue den Ansturm ab, und Worker bauen ihn in einem tragbaren Tempo ab. Synchroner Code bricht unter so einer Lastspitze einfach zusammen.
Konkretes Beispiel: Ein Checkout-Flow, den wir für einen Kunden neu gebaut haben, feuerte früher 6 Drittanbieter-Aufrufe inline ab (Steuer, Betrugsprüfung, Fulfillment, E-Mail, SMS, Analytics). Der mediane Checkout dauerte 1,4 Sekunden, und der p99-Wert lag bei 8 Sekunden, sobald ein Anbieter langsam wurde. Nachdem wir alles außer Steuer und Betrugsprüfung in BullMQ-Worker verlagert hatten, fiel der Median auf 220ms, und der p99-Wert blieb unter 600ms.
Observability vor cleverer Architektur
Der Instinkt beim Skalieren ist, clevere Systeme zu entwerfen. Der richtige Schritt ist, das vorhandene System so lange zu instrumentieren, bis genau erkennbar ist, wo die Zeit verloren geht.
Unser Standard:
- OpenTelemetry-Tracing, exportiert nach Honeycomb oder Tempo. Jeder Request hat eine Trace-ID. Jeder externe Aufruf ist ein Span.
- Strukturierte Logs im JSON-Format, mit Trace-ID. Pino reicht dafür völlig.
- RED-Metriken pro Endpunkt – Rate, Errors, Duration. Grafana-Dashboards. Alerts auf p99, nicht auf p50.
- Synthetische Checks von außerhalb des eigenen Netzwerks, die jede Minute gegen kritische Pfade laufen.
Sie werden Ihre Flaschenhälse nicht korrekt vorhersagen. Sie werden sie messen. Das Team, das zuerst Observability ausliefert, gewinnt die nächsten sechs Monate an Skalierungsarbeit.
Die Fallstricke, die wir immer wieder gesehen haben
Eine kurze Liste an Dingen, die uns schon Produktionsausfälle beschert haben:
- Zu viel loggen, dadurch die Log-Pipeline lahmlegen und am Ende ganz ohne Logs dastehen.
- Vergessen, Nodes
--max-old-space-sizezu setzen. Der Standardwert liegt bei 1,5GB. Ihr Container hat 8GB. Rechnen Sie selbst. - Schreibzugriffe auf eine Datenbank in einer Promise.all-Schleife, ohne die Nebenläufigkeit zu begrenzen. Connection-Pool-Erschöpfung im ungünstigsten Moment.
- Health-Checks, die grün sind, obwohl der Service tatsächlich kaputt ist. Testen Sie die Abhängigkeitskette, nicht nur den Prozess.
- Sich darauf verlassen, dass ein Drittanbieter-SDK die Timeouts setzt. Immer mit eigenen Timeouts umschließen.
Die unsexy Wahrheit
Bei 10M Requests pro Tag liegt der Unterschied zwischen einem System, das funktioniert, und einem, das es nicht tut, selten an architektonischem Genie. Es liegt an einem Team, das seine Dashboards im Blick behält, langsame Queries in der Woche behebt, in der sie auftauchen, Incident-Reviews ehrlich durchführt und dem Drang widersteht, zu refactoren, wenn eigentlich instrumentiert werden sollte.
Stimmt diese Kultur, hält Node.js deutlich länger durch, als es die Konferenzvorträge vermuten lassen. Genau diese Art von Arbeit übernehmen unsere Teams für Backend und Cloud & DevOps bei Produkten in der Wachstumsphase.



