php-fpm tuning entscheidet, wie viele PHP-FPM-Prozesse gleichzeitig laufen dürfen, wie schnell neue Prozesse starten und wie lange sie Anfragen bedienen. Ich zeige dir, wie du pm.max_children, pm, pm.start_servers, pm.min_spare_servers, pm.max_spare_servers und pm.max_requests so einstellst, dass deine Anwendung unter Last zügig reagiert und der Server nicht ins Swapping rutscht.
Zentrale Punkte
- pm-Modus: static, dynamic oder ondemand richtig wählen, damit Prozesse passend zu deinem Traffic bereitstehen.
- pm.max_children: Zahl der gleichzeitigen PHP-Prozesse am RAM und am realen Prozessverbrauch ausrichten.
- Start-/Spare-Werte: pm.start_servers, pm.min_spare_servers, pm.max_spare_servers sinnvoll balancieren.
- Recycling: Mit pm.max_requests Speicherlecks abfedern, ohne unnötigen Overhead zu erzeugen.
- Monitoring: Logs, Status und RAM im Blick behalten, dann schrittweise nachregeln.
Warum das Prozess-Management zählt
Ich steuere mit PHP-FPM die Ausführung jedes PHP-Skripts als eigenen Prozess, und jede parallele Anfrage braucht ihren eigenen Worker. Ohne passende Limits blockieren Anfragen in Warteschlangen, was zu Timeouts und Fehlern führt. Setze ich die Obergrenzen zu hoch, frisst der Prozesspool den Arbeitsspeicher auf und der Kernel beginnt zu swappen. Diese Balance ist kein Ratespiel: Ich orientiere mich an realen Messwerten und halte eine Sicherheitsmarge. So bleibt die Latenz niedrig und der Durchsatz stabil, auch wenn die Last springt.
Wichtig ist mir eine klare Zielgröße: Wie viele gleichzeitige PHP-Ausführungen will ich ermöglichen, ohne RAM zu erschöpfen? Gleichzeitig prüfe ich, ob Engpässe eher in der Datenbank, bei externen APIs oder im Webserver liegen. Nur wenn ich den Flaschenhals kenne, wähle ich richtige Werte für pm, pm.max_children und Co. Ich starte konservativ, messe und erhöhe dann schrittweise. So vermeide ich harte Neustarts und unerwartete Ausfälle.
Die drei pm‑Modi: static, dynamic, ondemand
Der Modus static hält immer exakt pm.max_children Prozesse bereit. Das liefert sehr vorhersehbare Latenzen, weil kein Startvorgang nötig ist. Ich nutze static, wenn die Auslastung sehr gleichmäßig ist und genug RAM zur Verfügung steht. Bei wechselnder Nachfrage verschwende ich in static jedoch leicht Speicher. Deshalb setze ich static gezielt dort ein, wo ich konstante Ausführung brauche.
Mit dynamic starte ich eine Startmenge und lasse die Poolgröße zwischen min_spare und max_spare atmen. Dieser Modus eignet sich für Traffic mit Wellen, weil Worker nach Bedarf entstehen und wieder enden. Ich halte dabei immer genug Idle-Prozesse vor, um Spitzen ohne Wartezeit zu fangen. Zu viele Idle-Worker binden aber unnötig RAM, weshalb ich die Spare-Spanne eng führe. So bleibt der Pool beweglich, ohne aufzuquellen.
Im Modus ondemand existieren zunächst keine Worker, PHP-FPM startet sie erst bei Anfragen. Das spart Speicher in Ruhephasen, dafür kostet der erste Treffer etwas Latenz. Ich wähle ondemand für selten aufgerufene Pools, Admin-Tools oder Cron-Endpunkte. Für stark frequentierte Websites liefert ondemand meist schlechtere Reaktionszeiten. Dort ziehe ich dynamic mit sauber gesetzten Spare-Werten klar vor.
pm.max_children richtig dimensionieren
Ich rechne pm.max_children aus dem verfügbaren RAM für PHP und dem durchschnittlichen Speicher pro Worker. Dazu reserviere ich zuerst Speicher für System, Webserver, Datenbank und Caches, damit das System nicht in die Auslagerung läuft. Den verbleibenden RAM teile ich durch den real gemessenen Prozessverbrauch. Aus der Theorie ziehe ich 20–30 % Sicherheitsmarge ab, um Ausreißer und Lastspitzen abzufangen. Das Ergebnis nutze ich als Startwert und beobachte danach die Wirkung.
Den mittleren Prozessverbrauch ermittle ich mit Tools wie ps, top oder htop und schaue auf RSS/RES. Wichtig: Ich messe unter typischer Last, nicht im Leerlauf. Wenn ich viele Plugins, Frameworks oder große Libraries lade, klettert der Verbrauch je Worker spürbar. Außerdem begrenzt die CPU die Kurve: Mehr Prozesse helfen nicht, wenn eine Single-Thread-Leistung der CPU pro Anfrage limitiert. Wer tiefer in die CPU-Charakteristik einsteigen möchte, findet Hintergründe zur Single-Thread-Performance.
Ich halte meine Annahmen transparent: Wie viel RAM steht PHP wirklich zur Verfügung? Wie groß ist ein Worker bei typischen Requests? Welche Spitzen treten auf? Stimmen die Antworten, setze ich pm.max_children, mache einen sanften Reload und kontrolliere RAM, Antwortzeiten sowie Fehlerraten. Erst danach gehe ich in kleinen Schritten weiter nach oben oder unten.
Richtwerte nach Servergröße
Die folgende Tabelle gibt mir Startwerte an die Hand. Sie ersetzt kein Messen, liefert aber solide Orientierung für erste Einstellungen. Ich passe die Werte je Anwendung an und prüfe sie mit Monitoring. Bleiben Reserven ungenutzt, erhöhe ich behutsam. Kommt der Server an die RAM-Grenze, ziehe ich die Werte zurück.
| Server-RAM | RAM für PHP | Ø MB/Worker | pm.max_children (Start) | Einsatz |
|---|---|---|---|---|
| 1–2 GB | ~1 GB | 50–60 | 15–20 | Kleine Sites, Blogs |
| 4–8 GB | ~4–6 GB | 60–80 | 30–80 | Business, kleine Shops |
| 16+ GB | ~10–12 GB | 70–90 | 100–160 | Hochlast, API, Shops |
Ich lese die Tabelle von rechts nach links: Passt der Einsatz zum Projekt, überprüfe ich, ob RAM für PHP realistisch reserviert ist. Dann wähle ich eine Worker-Größe, die zu Codebasis und Erweiterungen passt. Danach setze ich pm.max_children und beobachte die Wirkung im Livebetrieb. Trefferquote und Stabilität steigen, wenn ich diese Schritte sauber dokumentiere.
Start-, Spare- und Requests-Werte einstellen
Mit pm.start_servers lege ich fest, wie viele Prozesse sofort bereitstehen. Zu niedrig erzeugt Kaltstarts unter Last, zu hoch bindet unnötig RAM. Ich richte mich oft an 15–30 % von pm.max_children aus und runde ab, wenn die Last eher ruhig startet. Bei Traffic-Spitzen wähle ich eine etwas höhere Startmenge, damit Anfragen nicht anrollen, bevor genügend Worker warten. Diese Feinjustierung senkt die erste Antwortzeit deutlich.
Die Werte pm.min_spare_servers und pm.max_spare_servers definieren die Idle-Spanne. Ich halte so viele freie Worker vor, dass neue Anfragen direkt zugreifen, aber nicht so viele, dass die Leerlaufprozesse Speicher verschwenden. Bei Shops setze ich gern ein engeres Fenster, um Spitzen zu glätten. Mit pm.max_requests recycle ich Prozesse nach einigen hundert Requests, um Memory-Drift zu begrenzen. Für unauffällige Anwendungen wähle ich 500–800, bei Verdacht auf Lecks gehe ich bewusst niedriger.
Monitoring und Fehlersuche
Ich prüfe regelmäßig Logs, Statusseiten und RAM. Warnungen zu erreichten pm.max_children-Limits sind für mich ein klares Signal, die Obergrenze anzuheben oder Code/DB zu optimieren. Häufen sich 502/504-Fehler, sehe ich in die Webserver-Logs und in die Warteschlangen. Deutliche Schwankungen der Latenz deuten auf zu wenige Prozesse, auf blockierende I/O oder auf zu hohe Prozesskosten hin. Ich schaue zuerst auf harte Fakten und reagiere dann mit kleinen Schritten, nie mit XXL-Sprüngen.
Engpässe erkenne ich schneller, wenn ich die Wartezeiten entlang der gesamten Kette messe: Webserver, PHP-FPM, Datenbank, externe Dienste. Steigt die Backend-Zeit nur bei bestimmten Routen, isoliere ich die Ursachen per Profiling. Treten Wartezeiten überall auf, setze ich an der Server- und Poolgröße an. Hilfreich ist zudem ein Blick auf Worker-Warteschlangen und auf Prozesse im D-Status. Erst wenn ich die Lage verstehe, verändere ich Limits – und dokumentiere jede Änderung sauber.
Webserver und PHP-FPM im Zusammenspiel
Ich achte darauf, dass Webserver-Limits und PHP-FPM harmonieren. Zu viele gleichzeitige Verbindungen im Webserver bei zu wenigen Workern verursachen Warteschlangen und Timeouts. Sind die Worker hochgesetzt, aber der Webserver limitiert die Annahme, bleibt Leistung liegen. Parameter wie worker_connections, event-Loop und Keep-Alive wirken direkt auf die PHP-Last. Einen praxisnahen Einstieg in die Feinabstimmung liefern Hinweise zu Threadpools im Webserver.
Ich behalte Keep-Alive-Zeitfenster im Blick, damit Leerlaufverbindungen nicht unnötig Worker blockieren. Für statische Assets setze ich aggressives Caching vor PHP, um Workload vom Pool fernzuhalten. Reverse-Proxy-Caches helfen zusätzlich, wenn identische Antworten häufig abgerufen werden. So kann ich pm.max_children niedriger halten und dennoch schneller ausliefern. Weniger Arbeit pro Anfrage ist oft die effektivste Stellschraube.
Feine Stellschrauben in php-fpm.conf
Ich gehe über die Grundwerte hinaus und justiere die Pool-Parameter fein. Mit pm.max_spawn_rate begrenze ich, wie schnell neue Worker entstehen dürfen, damit der Server bei Lastspitzen nicht zu aggressiv Prozesse startet und ins CPU-Thrashing rutscht. Für ondemand lege ich mit pm.process_idle_timeout fest, wie schnell ungenutzte Worker wieder verschwinden – zu kurz erzeugt Start-Overhead, zu lang bindet RAM. Beim listen-Socket entscheide ich zwischen Unix-Socket und TCP. Ein Unix-Socket spart Overhead und bietet saubere Rechtevergabe über listen.owner, listen.group und listen.mode. Für beide Varianten setze ich listen.backlog ausreichend hoch, damit anrollende Bursts im Kernel-Puffer landen, statt sofort abgewiesen zu werden. Mit rlimit_files erhöhe ich bei Bedarf die Zahl offener Dateien je Worker, was bei vielen gleichzeitigen Up- und Downloads Stabilität bringt. Und wenn es Prioritäten braucht, nutze ich process.priority, um wenig kritische Pools CPU-seitig etwas nachrangig zu behandeln.
Slowlog und Schutz vor Hängern
Um zähe Requests sichtbar zu machen, aktiviere ich den Slowlog. Mit request_slowlog_timeout definiere ich die Schwelle (z. B. 2–3 s), ab der ein Stacktrace ins slowlog geschrieben wird. So finde ich blockierende I/O, teure Schleifen oder unerwartete Locks. Gegen echte Hänger setze ich request_terminate_timeout, das hart abbricht, wenn ein Request zu lange läuft. Ich halte diese Zeitfenster konsistent mit max_execution_time aus PHP und den Timeouts des Webservers, damit nicht ein Layer früher abreißt als der andere. In der Praxis starte ich konservativ, analysiere Slowlogs unter Last und passe die Schwellen schrittweise an, bis die Signale aussagekräftig sind, ohne den Log zu überfluten.
Opcache, memory_limit und ihr Einfluss auf die Größe der Worker
Ich beziehe den Opcache in meine RAM-Planung ein. Sein Shared-Memory-Bereich zählt nicht pro Worker, sondern wird von allen Prozessen gemeinsam genutzt. Größe und Fragmentierung (opcache.memory_consumption, interned_strings_buffer) beeinflussen die Warm-Up-Zeit und die Trefferquote deutlich. Ein gut dimensionierter Opcache senkt CPU- und RAM-Druck pro Anfrage, weil weniger Code neu kompiliert wird. Gleichzeitig beachte ich das memory_limit: Ein hoher Wert schützt zwar vor Out-of-Memory in Einzelfällen, steigert aber das theoretische Worst-Case-Budget je Worker. Ich plane darum mit gemessenem Durchschnitt plus Puffer, nicht mit dem nackten memory_limit. Features wie Preloading oder JIT erhöhen den Speicherbedarf – ich teste sie gezielt und kalkuliere den Mehrverbrauch in die pm.max_children-Rechnung ein.
Pools trennen und priorisieren
Ich teile Anwendungen auf mehrere Pools auf, wenn sich Lastprofile stark unterscheiden. Ein Pool für Frontend-Traffic, einer für Admin/Backend, ein dritter für Cron/Uploads: So isoliere ich Spitzen und vergebe differenzierte Limits. Für selten frequentierte Endpunkte setze ich ondemand mit kleinem Idle-Timeout, für das Frontend dynamic mit enger Spare-Spanne. Über user/group und ggf. chroot sorge ich für saubere Isolation, während Socket-Rechte regeln, welcher Webserver-Prozess zugreifen darf. Wo Prioritäten gefragt sind, erhält das Frontend mehr pm.max_children und ggf. eine neutrale process.priority, während Cron/Reports mit kleinerem Budget und niedrigerer Priorität laufen. So bleibt die Nutzeroberfläche reaktionsschnell, auch wenn im Hintergrund schwere Jobs arbeiten.
Statusendpunkte sauber nutzen
Für die Laufzeitdiagnose aktiviere ich pm.status_path und optional ping.path pro Pool. Im Status sehe ich Active/Idle-Worker, die Listen Queue, durchsatzbezogene Zähler und Slow-Request-Metriken. Eine dauerhaft wachsende Listen-Queue oder ständig 0 Idle-Worker sind für mich Alarmsignale. Ich schütze diese Endpunkte hinter Auth und einem internen Netz, damit keine Betriebsdetails nach außen gelangen. Zusätzlich aktiviere ich catch_workers_output, wenn ich kurzfristig stdout/stderr aus den Workern einsammeln will – etwa bei schwer reproduzierbaren Fehlern. Diese Signale kombiniere ich mit Systemmetriken (RAM, CPU, I/O), um zu entscheiden, ob ich pm.max_children erhöhe, Spare-Werte nachziehe oder an der Anwendung ansetze.
Besonderheiten in Containern und VMs
In Containern und kleinen VMs beachte ich cgroup-Limits und die Gefahr des OOM-Killers. Ich setze pm.max_children strikt nach dem Container-Memory-Limit und teste Lastspitzen, damit kein Worker abgeschossen wird. Ohne Swap in Containern ist die Sicherheitsmarge besonders wichtig. Bei CPU-Quotas skaliere ich die Workerzahl zur verfügbaren vCPU-Zahl: ist die Anwendung CPU-bound, bringt mehr Parallelität eher Warteschlangen als Durchsatz. IO-bound-Workloads vertragen mehr Prozesse, solange das RAM-Budget hält. Zusätzlich setze ich emergency_restart_threshold und emergency_restart_interval für den Master-Prozess, um eine Crash-Spirale abzufangen, falls ein seltener Bug mehrere Kinder in kurzer Zeit reißt. So bleibt der Dienst verfügbar, während ich die Ursache analysiere.
Reibungslose Deployments und Reloads ohne Ausfall
Ich plane Reloads so, dass laufende Requests sauber zu Ende geführt werden. Ein graceful reload (z. B. via systemd reload) übernimmt neue Konfigurationen, ohne offene Verbindungen hart zu beenden. Ich halte den Socket-Pfad stabil, damit der Webserver keinen Verbindungsabbruch sieht. Bei Versionswechseln, die viel Opcache invalidieren, wärme ich den Cache vor (Preloading/Warmup-Requests), um die Latenzspitzen direkt nach dem Deployment zu begrenzen. Größere Änderungen teste ich zuerst auf einem kleineren Pool oder in einer Canary-Instanz mit identischer Konfiguration, bevor ich die Werte flächig ausrolle. Jede Anpassung landet mit Zeitstempel und Metrik-Screenshots in meinem Änderungslog – das verkürzt die Fehlersuche, falls es unerwartete Nebenwirkungen gibt.
Burst-Verhalten und Warteschlangen
Lastspitzen pralle ich mit einem abgestimmten Warteschlangen-Design ab. Ich setze listen.backlog so hoch, dass der Kernel kurzfristig mehr Verbindungsversuche puffern kann. Auf Webserver-Seite limitiere ich die maximale Zahl gleichzeitiger FastCGI-Verbindungen pro Pool so, dass sie zu pm.max_children passt. Damit stauen sich Bursts lieber kurz im Webserver (günstig) als tief in PHP (teuer). Ich messe die Listen Queue im FPM-Status: Steigt sie regelmäßig, erhöhe ich entweder die Workerzahl, optimiere Cache-Trefferquoten oder senke aggressive Keep-Alive-Werte. Ziel ist, bei Spitzen den Time-to-First-Byte stabil zu halten, statt Requests in endlosen Queues versanden zu lassen.
Praxis-Workflow für Anpassungen
Ich starte mit einem Audit: RAM-Budget, Prozessgröße, I/O-Profile. Danach setze ich konservative Startwerte für pm.max_children und den pm-Modus. Anschließend fahre ich Lasttests oder beobachte reale Spitzenzeiten. Ich protokolliere alle Änderungen samt Metriken und Zeitfenstern. Nach jeder Justierung prüfe ich RAM, Latenz-P50/P95 und Fehlerraten – erst dann folgt der nächste Schritt.
Wenn ich wiederholt am Limit anstehe, eskaliere ich nicht sofort die Worker-Zahl. Zuerst optimiere ich Queries, Cache-Trefferquoten und teure Funktionen. Ich verschiebe IO-lastige Arbeiten in Queues und verkürze Antwortwege. Erst wenn die Anwendung effizient arbeitet, erhöhe ich die Poolgröße. Dieser Ablauf spart Ressourcen und vermeidet Folgeschäden an anderer Stelle.
Typische Szenarien: Beispielwerte
Auf einem 2‑GB‑vServer reserviere ich rund 1 GB für PHP-FPM und setze einen Worker-Verbrauch von etwa 50–60 MB an. Damit starte ich bei pm.max_children um 15–20 und nutze dynamic mit kleiner Startmenge. min_spare halte ich bei 2–3, max_spare bei 5–6. pm.max_requests setze ich auf 500, damit Prozesse regelmäßig getauscht werden. Diese Einstellungen liefern kleinen Projekten stabile Reaktionszeiten.
Bei 8 GB RAM plane ich meist 4–6 GB für PHP und setze Worker-Größen von 60–80 MB an. Daraus ergeben sich 30–80 Kinderprozesse als Startbereich. pm.start_servers liegt bei 15–20, min_spare bei 10–15, max_spare bei 25–30. pm.max_requests wähle ich zwischen 500 und 800. Unter Last prüfe ich, ob die RAM-Spitze Luft lässt, und erhöhe dann vorsichtig.
In Hochlast-Setups mit 16+ GB RAM reserviere ich 10–12 GB für FPM. Bei 70–90 MB pro Worker lande ich schnell bei 100–160 Prozessen. Ob static oder dynamic sinnvoll ist, hängt von der Lastform ab. Für dauerhaft hohe Auslastung überzeugt static, für wellenförmige Nachfrage dynamic. In beiden Fällen bleibt konsequentes Monitoring Pflicht.
Stolpersteine vermeiden und Prioritäten setzen
Ich verwechsel nicht die Zahl der Besucher mit der Zahl gleichzeitiger PHP-Skripte. Viele Seitenaufrufe treffen Caches, liefern statische Dateien oder blockieren außerhalb von PHP. Deshalb dimensioniere ich pm.max_children nach gemessener PHP-Zeit, nicht nach Sitzungen. Werden Prozesse zu sparsam gesetzt, sehe ich wartende Requests und steigende Fehlerraten. Bei zu hohen Werten kippt der Speicher in Swap und alles wird langsamer.
Ein häufiger Irrtum: Mehr Prozesse gleich mehr Speed. In Wahrheit zählt die Balance aus CPU, IO und RAM. Geht die CPU auf 100 % und die Latenz schnellt hoch, helfen weitere Worker kaum. Besser ist es, den echten Engpass zu beseitigen oder die Last per Cache zu drücken. Warum Worker oft der Engpass sind, erklärt der Ratgeber zu PHP-Worker als Flaschenhals.
Kurz zusammengefasst
Ich ermittle erst den realen RAM-Verbrauch pro Worker und setze daraus pm.max_children mit Puffer. Dann wähle ich den pm‑Modus passend zur Lastform und balanciere Start- sowie Spare-Werte. Mit pm.max_requests halte ich Prozesse frisch, ohne unnötigen Overhead. Logs, Status und Metriken leite ich in ein sauberes Monitoring, damit jede Änderung messbar bleibt. So erreiche ich kurze Antwortzeiten, stabile Pools und eine Serverlast, die Reserven für Spitzen hat.


