Mit nginx keepalive senke ich Verbindungsaufbaukosten, reduziere Handshakes und beschleunige Antwortzeiten spürbar. Gezielt abgestimmte Timeouts, Request-Limits je Verbindung und wiederverwendete Upstream-Sockets liefern messbare Leistungsgewinne ohne neue Hardware.
Zentrale Punkte
- Timeouts sinnvoll wählen: Idle-Zeit so kurz wie nötig, so lang wie nützlich.
- Requests pro Verbindung begrenzen: Langlebige Sockets, keine Hänger.
- Upstream-Pools aktivieren: Persistente Backend-Verbindungen pro Worker.
- Worker und Verbindungen abstimmen: Genug Slots für Idle und aktive Clients.
- Monitoring etablieren: Connect-Rate, Latenz und Fehler beobachten.
NGINX Keepalive: Wirkung und Kosten
Ich halte TCP-Verbindungen bewusst offen, weil Handshakes teuer sind und bei vielen kleinen Requests dominieren. Persistente Sockets sparen nicht nur RTTs, sie glätten auch die CPU-Last, da Kryptographie für TLS seltener anläuft. Jede offene Verbindung belegt allerdings Ressourcen, etwa File-Deskriptoren und Puffer, die ich im Blick behalten muss. Die Kunst liegt im Verhältnis: genug Reuse für Tempo, genug Budget für frische Verbindungen bei Lastspitzen. Wer das austariert, erzielt konstant kurze TTFB-Werte und ein schnelles Nutzergefühl.
HTTP/2 und HTTP/3: Multiplexing trifft Keepalive
Mit HTTP/2 und HTTP/3 sinkt die Zahl der nötigen Verbindungen pro Client, weil mehrere Streams über eine Leitung laufen. Keepalive bleibt dennoch relevant: Die eine Verbindung sollte verlässlich offen bleiben, sonst verpufft der Multiplexing-Vorteil durch häufige Neuverbindungen.
Ich achte auf dedizierte Idle-Parameter für moderne Protokolle und sorge dafür, dass die Werte zu meinen Client-Timeouts passen. Für Tests starte ich moderat und erhöhe bei stabiler Last, bis die Neuverbindungsrate sinkt und Latenzen konstant bleiben.
http {
# HTTP/2: Idle-Zeit für ungenutzte, aber geöffnete Streams
http2_idle_timeout 60s;
# HTTP/3/QUIC: ähnliche Logik für UDP-basierte Verbindungen
http3_idle_timeout 60s;
# TLS-Resumption reduziert Handshake-Kosten bei Neuverbindungen
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
}
Multiplexing senkt die benötigte Anzahl paralleler TCP/QUIC-Verbindungen, aber nicht die Bedeutung der richtigen Timeouts. Wer HTTP/2/3 nutzt, kann die Client-Timeouts oft etwas großzügiger wählen, weil viele kleine Ressourcen über denselben Kanal fließen. Wichtig: Messbar bleiben Time to First Byte, Fehlerquoten und offene Streams je Verbindung.
Client-Keepalive richtig einstellen
Für Browser-Clients steuere ich Wiederverwendung über keepalive_timeout und keepalive_requests, damit Sockets lange genug leben, ohne ewig zu blockieren. Als Startpunkt nutze ich 30–60 Sekunden Timeout und 100–300 Requests je Verbindung, dann passe ich anhand Metriken an. Eine ausführliche Einordnung liefert dieser Keepalive-Timeout Guide, der die Auswirkungen auf Latenz und Serverressourcen erklärt. Kürzere Timeouts eignen sich bei sehr vielen kurzen Aufrufen, längere Zeitfenster helfen bei periodischen API-Zugriffen. Für den Einstieg setze ich klare Defaults und messe die Wirkung auf offene Verbindungen sowie Fehlerbilder.
http {
# Idle-Verbindungen zum Client
keepalive_timeout 60s;
# Obergrenze an Requests je TCP-Verbindung
keepalive_requests 200;
# Optional: bestimmten Clients Keep-Alive entziehen (Legacy-Bugs)
# keepalive_disable msie6;
}
Upstream-Keepalive im Reverse Proxy
Zwischen NGINX und Backend-Apps nutze ich persistente Upstream-Sockets, weil der Aufbau zu PHP-FPM, Node.js oder Python-Services ebenfalls Latenz kostet. Dazu aktiviere ich im Upstream-Pool eine passende Anzahl wiederverwendbarer Verbindungen pro Worker. Wichtig sind HTTP/1.1 nach hinten und ein leerer Connection-Header, sonst bricht der Client-Wunsch „close“ die Backend-Persistenz. Ich orientiere mich an gleichzeitigen Anfragen und stelle den Pool so ein, dass kaum neue Connects anfallen. So sinkt die Backend-Connect-Time, und die gesamte Kette liefert flottere Antworten.
upstream backend {
server 127.0.0.1:9000;
keepalive 64; # Anzahl persistenter Upstream-Verbindungen pro Worker
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# TCP-Keepalive für Upstream-Sockets auf OS-Ebene
proxy_socket_keepalive on;
}
}
Pool-Dimensionierung und Verbindungsbudget
Ich berechne Pools realistisch: Die Zahl persistenter Upstream-Verbindungen ergibt sich aus worker_processes × keepalive je Upstream. Wer 8 Worker und keepalive 64 einsetzt, hält bis zu 512 Sockets pro Upstream offen – pro Instanz. Hinter einem Load-Balancer oder bei mehreren Upstream-Zielen kann sich das schnell summieren.
Mein Zielwert: genug offene Sockets, damit der Großteil der Anfragen ohne neuen Connect bedient wird, aber noch Luft für Spitzen bleibt. Ich beobachte die Metrik „neue Upstream-Verbindungen pro Sekunde“ und senke sie, bis weitere Erhöhungen der Poolgröße keine nennbare Latenzverbesserung mehr bringen.
Ich berücksichtige außerdem Fairness: Zu große Pools können frisch eintreffende Clients benachteiligen, weil Worker Slots mit Idle-Connections belegen. Ein moderates Limit mit aktivem Monitoring ist meist schneller als Maximalwerte auf Verdacht.
Feinabstimmung: Timeouts und Request-Limits
Ich kombiniere Timeout und Request-Limit so, dass Verbindungen effektiv wiederverwendet werden, ohne zum Langläufer zu werden. Hohe Werte auf beiden Achsen minimieren Connects, erhöhen aber das Risiko hängender Sockets bei Netzproblemen. Niedrige Werte sorgen für frische Verbindungen, kosten jedoch zusätzliche Handshakes. Ich taste mich in kleinen Schritten vor, beobachte Fehler und justiere in Intervallen. Die folgende Tabelle zeigt sinnvolle Startkorridore für unterschiedliche Nutzungsmuster und liefert eine kompakte Orientierung.
| Szenario | keepalive_timeout | keepalive_requests | Hinweis |
|---|---|---|---|
| Viele kurze Seitenaufrufe | 10–30s | 100–300 | Schneller Reuse, geringe Idle-Bindung |
| Typische Website | 60–120s | 200–400 | Gutes Mittelmaß für Assets und HTML |
| API mit periodischen Calls | 60–120s | 300–1000 | Höhere Reuse-Quote für Clients |
| Interne Services / Gateways | 30–90s | 500–1000+ | Konstanz wichtiger als minimale Connects |
Worker-Tuning und Verbindungen
Ich stelle worker_processes auf auto oder auf die Anzahl CPU-Kerne und plane genügend worker_connections ein, weil Idle-Sockets Slots belegen. Zu niedrige Limits verhindern neue Annahmen, obwohl noch CPU-Kapazität vorhanden wäre. Wer hohe Keepalive-Pools fährt, braucht ausreichend Deskriptoren und Event-Slots je Worker. Eine gute Einführung liefert „Worker-Connections skalieren“, die Zusammenhänge zwischen Events, Verbindungen und Last erläutert. Durchdachte Werte stellen sicher, dass Idle-Reuse und Neuverbindungen nebeneinander Platz finden.
worker_processes auto;
events {
worker_connections 4096;
# Optional: reuseport kann Verteilung auf Kernel-Ebene verbessern
# multi_accept on;
}
http {
keepalive_timeout 60s;
keepalive_requests 200;
upstream backend {
server 127.0.0.1:9000;
keepalive 64;
}
}
Betriebssystem- und Socket-Tuning
Ich prüfe Systemlimits, damit Keepalive sein Potenzial entfalten kann. Zu wenig Deskriptoren oder enge Socket-Queues erzeugen künstliche Engpässe. Neben ulimit und worker_rlimit_nofile sind Kernelschranken entscheidend.
# Beispielhafte sysctl-Werte (mit Vorsicht und Tests anpassen)
fs.file-max = 1000000
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 16384
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 262144
Ich kalibriere diese Werte zur Umgebung: viele kurzlebige Verbindungen profitieren von weiter Port-Range und kurzen FIN-/TIME_WAIT-Zeiten. Bei Upstream-Keepalive reduziere ich Neuconnects, wodurch TIME_WAIT-Drücke kleiner werden. Zusätzlich berücksichtige ich NAT-Geräte zwischen Proxy und Backend: Zu aggressive Idle-Timeouts im Netz kappen Verbindungen unvorhersehbar. Ein moderates Request-Limit pro Socket und TCP-Keepalives (proxy_socket_keepalive on;) beugen „stale“ Verbindungen vor.
Header und HTTP-Version korrekt setzen
Ich achte auf HTTP/1.1 zum Backend, weil Upstream-Keepalive nur damit funktioniert. Zudem entferne ich aktive Verbindungssteuerung per Header, damit NGINX die Persistenz eigenständig verwaltet. Auf Client-Seite lasse ich Keep-Alive standardkonform laufen und limitiere die Lebenszeit über Timeout und Request-Limit. Ergänzend prüfe ich Backend-Idle-Timeouts und setze sie minimal höher als NGINX, um Reset-Fehler zu vermeiden. Saubere Header sichern die Wiederverwendung ohne ungewollte Schließungen.
# Beispiel: Proxy-Location mit korrekten Headern
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
Abgrenzung: HTTP Keep-Alive vs. TCP-Keepalive
Ich unterscheide streng zwischen HTTP Keep-Alive (mehrere HTTP-Requests pro Verbindung) und TCP-Keepalive (Probes auf OS-Ebene, um tote Gegenstellen zu erkennen). HTTP Keep-Alive steuere ich mit keepalive_timeout und keepalive_requests, während TCP-Keepalives je nach Stack über proxy_socket_keepalive on; und Systemparameter laufen. Für Backends über instabile Netze aktiviere ich TCP-Keepalives, um hängende Sockets schneller zu räumen.
Langläufer und Sonderfälle: WebSockets, SSE, gRPC
WebSockets und Server-Sent Events sind Langläufer, die eine Verbindung über lange Zeit offen halten – hier spielt klassischer Reuse eine untergeordnete Rolle. Ich sorge für passende proxy_read_timeout und schütze mich mit send_timeout gegen Slowloris-Effekte. Für gRPC (HTTP/2-basiert) gelten die Multiplexing-Erwägungen; Idle-Timeouts richte ich so aus, dass Streams nicht unnötig weggekickt werden.
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
send_timeout 30s;
}
Monitoring und Metriken
Ich messe den Erfolg über Kennzahlen wie Rate neuer Upstream-Verbindungen, upstream_connect_time und Anteile offener Verbindungen je Worker. Sinkende Connect-Raten bei gleichbleibenden oder steigenden Requests deuten auf erfolgreichen Reuse hin. Auffällige Timeouts oder Connection-Resets signalisieren widersprüchliche Timeouts zwischen NGINX und Backend. Zusätzlich beobachte ich Speicher, File-Deskriptoren und Event-Queues unter Last. Wer regelmäßig prüft, entdeckt Trends früh und verhindert teure Ausfälle.
Logging-Verbesserungen für Reuse-Transparenz
Für mehr Einblick erweitere ich das Access-Log um Verbindungsdetails. So erkenne ich, wie oft eine TCP-Verbindung wiederverwendet wird und wie sich Connect-Zeiten entwickeln.
log_format keepalive_fmt
'$remote_addr $host "$request" $status $body_bytes_sent '
'$request_time $upstream_connect_time '
'conn:$connection reqs:$connection_requests';
access_log /var/log/nginx/access_keepalive.log keepalive_fmt;
Ich beobachte Median- und P95/P99-Werte von upstream_connect_time sowie die Verteilung von $connection_requests. Steigende Reuse-Zahlen bei stabiler Latenz bedeuten, dass Pools und Timeouts passend gewählt sind.
Typische Stolpersteine und Lösungen
Zu große Pools füllen Verbindungs-Slots, während neue Clients warten, deshalb halte ich Größen moderat und messe. Unterschiedliche Idle-Timeouts zwischen Proxy und Backend erzeugen Resets, also setze ich das Backend minimal höher als NGINX. Ein vergessenes „Connection: close“ im Proxy-Header kappt Persistenz, daher leere ich den Header konsequent. TLS-Negotiation kann bei vielen Neuverbindungen die CPU belasten, was ich durch höheren Reuse-Anteil entschärfe. Bei sporadischen Netzfehlern hilft ein moderates Request-Limit pro Socket, damit alte Sessions nicht unendlich leben.
Praxisnahe Konfigurationen
Für stark frequentierte Websites wähle ich ein kurzes Timeout und ein mittelhohes Request-Limit, damit Assets effizient laufen. Bei APIs mit wiederkehrenden Calls hebe ich das Limit an, um TCP- und TLS-Handshakes weiter zu senken. Upstream-Pools dimensioniere ich anhand erwarteter Parallelität und teste mit realistischem Traffic. Jede Umgebung verhält sich anders, deshalb überprüfe ich nach Änderungen die Latenz und Fehlerbilder. Zwei Beispiele zeigen Startwerte, die ich anschließend mit Metriken schärfe.
# Szenario 1: Stark frequentierte Website
http {
keepalive_timeout 30s;
keepalive_requests 300;
upstream app {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 443 ssl http2;
# HTTP/2-Idle im Blick behalten
http2_idle_timeout 45s;
}
}
# Szenario 2: API mit periodischen Calls
http {
keepalive_timeout 75s;
keepalive_requests 1000;
upstream api_backend {
server 127.0.0.1:9001;
keepalive 64;
}
server {
listen 443 ssl http2;
# Etwas längeres Idle-Fenster für wiederkehrende Aufrufe
http2_idle_timeout 75s;
}
}
Checkliste für iterative Optimierung
Ich starte mit einer Analyse des Status quo: Traffic-Muster, Antwortzeiten und Fehlerquote geben den Takt vor. Danach setze ich Client-Timeout und Request-Limit auf solide Startwerte und aktiviere Upstream-Pools. Backend-Idle-Timeouts stelle ich etwas höher ein als NGINX, damit keine unerwarteten Resets auftreten. Anschließend überwache ich Verbindungsraten, Connect-Time und offene Sockets pro Worker. Wer den Reuse-Grad vertiefen will, findet Anregungen rund um Connection Reuse und sinnvolle Obergrenzen.
Zusätzliche Diagnose: Mismatchs und Zeitverhalten
Wenn Verbindungen scheinbar „grundlos“ abbrechen, suche ich nach Mismatchs in der Kette: Client-Idle vs. NGINX-Timeout vs. Backend-Idle und zwischengeschaltete NAT/Gateways. Ich erhöhe das Backend-Timeout leicht über den NGINX-Wert, prüfe Reset-Codes im Error-Log und beobachte, ob upstream_connect_time Spitzen zeigt. Häufig reicht ein kleiner Puffer (z. B. +10–20%) beim Backend-Timeout, um Resets zu eliminieren.
Ich beachte außerdem „lingering close“-Phasen: Beim Schließen lässt NGINX eingehende Daten kurz auslaufen, was Worker-Ressourcen belegt. Sehr viele gleichzeitige Schließungen können Events binden. In solchen Fällen kalibriere ich Schließzeitfenster und halte die Gesamtzahl offener Verbindungen durch sinnvolle Keepalive-Werte im Lot.
Zusammenfassung: Keepalive als Performance-Hebel
Ich setze Keepalive gezielt ein, weil es Verbindungsaufbaukosten senkt, Latenz verringert und die CPU entlastet. Die Mischung aus passendem Timeout, sauberem Request-Limit und passenden Upstream-Pools bringt spürbare Geschwindigkeit. Ohne Monitoring bleibt Potenzial ungenutzt, daher prüfe ich Kennzahlen kontinuierlich und passe Werte Schritt für Schritt an. Wer zusätzliche Reserven braucht, achtet auf Worker-Anzahl, Verbindungs-Slots und korrekte Header-Behandlung. Professionelle Setups, etwa bei webhoster.de, schöpfen diese Stellschrauben konsequent aus und liefern schnelle, verlässliche Dienste.


