PHP Session Garbage Collection: Warum es deine Website blockieren kann

Die php session gc kann Anfragen blockieren, weil sie beim Aufräumen zehntausender Session-Dateien den PHP-Prozess lange bindet und dadurch andere Requests warten. Ich zeige, wie die probabilistische Bereinigung, Dateisperren und langsame I/O zu spürbaren Lags führen und wie ich diese Verzögerungen mit klaren Einstellungen, Cron-Jobs und RAM‑Storage vermeide, damit die Website flüssig bleibt.

Zentrale Punkte

  • Problemursache: Probabilistische GC, Datei‑I/O und Locks führen zu Wartezeiten.
  • Risikofaktor: Viele Sessions (z. B. 170.000) verlängern jeden GC‑Lauf.
  • WordPress: Admin + Heartbeat verschärfen Verzögerungen.
  • Hosting: RAM, SSD und Isolation mindern GC‑Kosten.
  • Lösung: Cron‑Bereinigung und Redis beschleunigen Requests.

PHP Session Garbage Collection kurz erklärt

Sessions speichern Statusdaten zwischen Requests, meist als Dateien im Dateisystem. Die Garbage Collection entfernt veraltete Dateien, deren Änderungszeit älter als session.gc_maxlifetime ist, häufig 1440 Sekunden. Standardmäßig startet PHP diese Bereinigung probabilistisch über session.gc_probability und session.gc_divisor, oft als 1 von 1000 Aufrufen. Klingt harmlos, doch bei starkem Traffic trifft es ständig jemanden, der den gesamten Lauf ertragen muss. Je mehr Dateien im Session‑Verzeichnis liegen, desto länger blockiert die Bereinigung den Prozess.

Warum blockiert die Bereinigung Anfragen?

Ein GC‑Lauf muss das Session‑Verzeichnis listen, jede Datei prüfen und alte Einträge löschen, was auf langsamen I/O schnell Sekunden kostet. Liegen 170.000 Dateien vor, arbeiten viele Systemaufrufe hintereinander, die CPU, RAM und Storage beanspruchen. Parallel gestartete PHP‑Prozesse versuchen mitunter zeitgleich zu löschen und verursachen zusätzliche Datei‑Locks. Das verschärft Wartezeiten, weil Prozesse einander ausbremsen oder blockieren. Wer tiefer in Session-Locking einsteigt, erkennt, wie stark Locking das Antwortzeitprofil prägt und Time‑to‑First‑Byte nach oben treibt, besonders bei Lastspitzen, die ich vermeiden will, indem ich die GC entkopple.

WordPress: langsame Admin-Seiten durch Sessions

Der Admin‑Bereich benötigt mehr CPU und Datenbankzugriffe als das Frontend, was jede extra Verzögerung spürbar macht. Wenn genau dann die Garbage Collection startet, steigt die Zeit bis zur fertigen HTML‑Ausgabe deutlich. Das Heartbeat‑API fragt zusätzlich den Server an und kollidiert bei Pech mit einem GC‑Lauf. Dadurch fühlt sich das Backend zäh an, und Klicks brauchen länger, obwohl die eigentliche Logik nicht viel tut. Ich entschärfe das, indem ich die Wahrscheinlichkeit der GC in Requests auf null setze und die Aufräumarbeit planbar außerhalb der Antwortzeiten starte.

Hosting-Leistung und Infrastruktur

Auf geteilten Systemen teilen sich viele Projekte I/O‑Kapazität, wodurch ein einzelner GC‑Lauf andere Websites bremst. Bessere Hardware mit schnellem NVMe‑Storage und genügend RAM reduziert die Kosten pro Dateizugriff. Eine saubere Isolation pro Kunde oder Container verhindert, dass fremde Lastspitzen dein Projekt erfassen. Ich prüfe zudem Prozesslimits und I/O‑Scheduler, damit viele gleichzeitige PHP‑Worker nicht ins Stocken geraten. Wer tiefer planen will, findet bei einer fokussierten Hosting-Optimierung konkrete Ansatzpunkte, um GC‑Läufe zu entkoppeln und die Latenz zu stabilisieren.

Sessions im Dateisystem vs. RAM-Stores

Dateibasierte Sessions sind simpel, verursachen aber viel Overhead beim Suchen, Prüfen und Löschen. RAM‑basierte Stores wie Redis oder Memcached verwalten Schlüssel effizient, liefern schnell und haben eingebaute Expiration‑Mechanismen. Das spart Systemaufrufe, verkürzt Latenzen und drückt die Fehleranfälligkeit durch Dateisperren. Ich ziehe RAM‑Storage vor, sobald Besucherzahlen steigen oder der Admin‑Bereich zäh reagiert. Eine Umstellung gelingt zügig, und ein Leitfaden für Session-Handling mit Redis hilft, die Konfiguration klar und die Ressourcen besser auszunutzen.

Sinnvolle PHP-Einstellungen für Sessions

Ich stelle die Garbage Collection so ein, dass keine Anfrage sie zufällig auslöst. Dazu setze ich die Wahrscheinlichkeit auf null, plane die Bereinigung per Cron und justiere die Lebensdauer passend zum Risiko. Außerdem aktiviere ich strikte Modi, damit PHP nur gültige IDs akzeptiert. Speicher und Pfad prüfe ich, damit keine langsamen NFS‑Mounts oder überfüllten Verzeichnisse bremsen. Die folgende Übersicht zeigt gängige Defaults und erprobte Werte, die ich je nach Anwendungsfall wähle, um die Performance messbar zu verbessern.

Einstellung Typischer Standard Empfehlung Wirkung
session.gc_maxlifetime 1440 Sekunden 900–3600 Sekunden Kürzere Lebensdauer reduziert alte Dateien und senkt I/O.
session.gc_probability / session.gc_divisor 1 / 1000 (häufig) 0 / 1 Keine Bereinigung in Requests, Cron übernimmt Cleanup.
session.save_handler files redis oder memcached RAM‑Storage reduziert Dateisperren und verkürzt Latenzen.
session.use_strict_mode 0 1 Nur gültige IDs, weniger Kollisionen und Risiken.
session.save_path Systempfad Eigener schneller Pfad Kurze Directory‑Tiefe, lokale SSD, weniger Stat-Aufrufe.

Zusätzlich beachte ich weitere Schalter, die Stabilität und Sicherheit verbessern, ohne Overhead zu erzeugen:

  • session.use_only_cookies=1, session.use_cookies=1 für klare Cookie‑Nutzung ohne URL‑IDs.
  • session.cookie_httponly=1, session.cookie_secure=1 (bei HTTPS) und ein passendes session.cookie_samesite (meist Lax), um Leaks zu vermeiden.
  • session.lazy_write=1, um unnötige Schreibzugriffe zu sparen, wenn sich der Inhalt nicht ändert.
  • session.serialize_handler=php_serialize für moderne Serialisierung und Interoperabilität.
  • session.sid_length und session.sid_bits_per_character anheben, um IDs robuster zu machen.

Konkrete Konfiguration: php.ini, .user.ini und FPM

Ich verankere die Einstellungen dort, wo sie zuverlässig greifen: global in der php.ini, per Pool in PHP‑FPM oder lokal per .user.ini in Projekten, die getrennte Bedürfnisse haben. Ein pragmatisches Set sieht so aus:

; php.ini oder FPM-Pool
session.gc_probability = 0
session.gc_divisor     = 1
session.gc_maxlifetime = 1800
session.use_strict_mode = 1
session.use_only_cookies = 1
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.lazy_write = 1
; schneller, lokaler Pfad oder RAM-Store
; session.save_handler = files
; session.save_path = "2;/var/lib/php/sessions"

Im FPM‑Pool kann ich Werte hart setzen, damit einzelne Apps sie nicht überschreiben:

; /etc/php/*/fpm/pool.d/www.conf
php_admin_value[session.gc_probability] = 0
php_admin_value[session.gc_divisor] = 1
php_admin_value[session.gc_maxlifetime] = 1800
php_admin_value[session.save_path] = "2;/var/lib/php/sessions"

Dateisystem und Save-Path-Layout

Große Verzeichnisse mit hunderttausenden Dateien sind langsam. Ich shardiere darum das Session‑Verzeichnis in Unterordner, damit die Directory‑Lookups kurz bleiben:

session.save_path = "2;/var/lib/php/sessions"

Die führende 2 erzeugt zwei Ebenen Unterordner, basierend auf Hash‑Teilen der Session‑ID. Zusätzlich helfen Mount‑Optionen wie noatime sowie ein Dateisystem mit guten Directory‑Indizes. Ich vermeide NFS für Sessions, wenn möglich, oder erzwinge Sticky Sessions am Load‑Balancer, bis ein RAM‑Store produktiv ist.

Locking im Code entschärfen

Viele Lags entstehen nicht nur durch GC, sondern durch unnötig lange gehaltene Locks. Ich öffne die Session so kurz wie möglich:

<?php
session_start();           // lesen
$data = $_SESSION['key'] ?? null;
session_write_close();     // Lock früh lösen

// teure Arbeit ohne Lock
$result = heavy_operation($data);

// nur bei Bedarf erneut öffnen und schreiben
session_start();
$_SESSION['result'] = $result;
session_write_close();

Wenn ich nur lese, starte ich die Session mit read_and_close, damit PHP gar nicht erst in den Schreibmodus geht:

<?php
session_start(['read_and_close' => true]);
// nur lesen, kein Schreiben nötig

So sinkt die Wahrscheinlichkeit, dass parallele Requests aufeinander warten müssen. In WordPress‑Plugins prüfe ich, ob session_start() überhaupt nötig ist, und verschiebe den Aufruf in späte Hooks, damit der Kernfluss nicht blockiert.

RAM-Store-Konfiguration für Sessions

Bei Redis oder Memcached beachte ich Timeouts, Datenbanken und Speicherpolitik. Ein robustes Beispiel für Redis sieht so aus:

session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?database=2&timeout=2&read_timeout=2&persistent=1"
session.gc_maxlifetime = 1800
session.gc_probability = 0
session.gc_divisor = 1

Weil RAM‑Stores Ablaufzeiten selbst managen, erspare ich mir Datei‑GC. Ich betreibe Sessions getrennt von Caches (andere DB oder Schlüsselpräfix), damit Evictions für Cache‑Schlüssel nicht ungewollt Sessions verwerfen. Die Speicherpolitik richte ich auf volatile‑LRU aus, sodass nur Schlüssel mit TTL verdrängt werden, wenn der Speicher knapp wird.

Externe Bereinigung per Cron: so entzerre ich Requests

Die sicherste Entkopplung erreiche ich, indem ich die GC außerhalb des Request‑Flows starte. Ich setze die Wahrscheinlichkeit im PHP‑ini oder per .user.ini auf 0 und rufe regelmäßig ein kleines Script per Cron auf, das die Bereinigung anstößt. Der Cron läuft idealerweise jede Minute oder alle fünf Minuten, abhängig vom Traffic und der gewünschten Hygiene. Wichtig bleibt, dass der Cron mit dem gleichen Benutzer wie der Webserver arbeitet, damit Berechtigungen stimmen. Zusätzlich kontrolliere ich Logs und Metriken, um sicherzugehen, dass die geplante Routine zuverlässig läuft.

Für dateibasierte Sessions nutze ich zwei bewährte Varianten:

  • Ein PHP‑Einzeiler, der die interne GC aufruft (ab PHP 7.1):
*/5 * * * * php -d session.gc_probability=1 -d session.gc_divisor=1 -r 'session_gc();' 2>/dev/null
  • Ein find‑Cleanup, der die mtime gegen die gewünschte Lifetime vergleicht:
*/5 * * * * find /var/lib/php/sessions -type f -mmin +30 -delete

Ich halte die Cron‑Laufzeit im Blick. Wenn fünf Minuten nicht genügen, erhöhe ich die Frequenz oder senke die Lifetime. In stark frequentierten Setups läuft der Cron minütlich, um das Verzeichnis klein zu halten.

Diagnose und Monitoring

Ich erkenne GC‑Spitzen an erhöhten Antwortzeiten und auffälligen I/O‑Peaks im Monitoring. Tools im WordPress‑Kontext wie Query Monitor helfen, langsam wirkende Hooks, Plugins und Admin‑Aufrufe zu identifizieren. Ein Blick in access‑ und error‑Logs zeigt, wann Requests deutlich länger dauern. Viele kleine 200‑ms‑Spitzen sind normal, doch sekundenlange Ausreißer weisen auf Locking oder GC hin. Wer zusätzlich Dateianzahl und Verzeichnisgröße beobachtet, sieht, wie sich das Session‑Verzeichnis füllt und warum ein geplanter Cleanup nötig ist.

Praktische Hilfsmittel für die Ursachensuche:

  • php-fpm slowlog und request_slowlog_timeout aktivieren, um blockierende Stellen zu sehen.
  • iotop, iostat, pidstat und vmstat, um I/O‑Druck und Kontextwechsel zu erkennen.
  • strace -p <pid> kurzfristig, um offene Dateien und Locks zu beobachten.
  • find | wc -l auf dem Session‑Pfad, um die Dateimenge zu messen.
  • TTFB‑ und p95/p99‑Latenzen im APM, um Verbesserungen nach Umstellung zu quantifizieren.

WordPress-spezifische Checks

Ich prüfe Plugins, die session_start() früh aufrufen, und ersetze Kandidaten mit unnötiger Session‑Nutzung durch Alternativen. Im Admin reduziere ich die Heartbeat‑Frequenz oder begrenze sie auf die Editorseiten. Caches dürfen Sessions nicht umgehen, sonst verpufft der Effekt; darum kontrolliere ich Ausnahmen sorgfältig. Auch wichtig: keine Session für Gäste, wenn es keinen Grund gibt. So sinkt die Dateimenge pro Tag spürbar, und der geplante GC hat weniger zu tun.

In WooCommerce‑Umgebungen schaue ich besonders auf Warenkorb‑ und Fragment‑Features, die Sessions für anonyme Nutzer erzeugen. Oft reicht es, Sessions erst bei echten Interaktionen (Login, Checkout) zu starten. Zusätzlich stelle ich sicher, dass WP‑Cron nicht parallel viel Last verursacht: Ich lasse WP‑Cron von einem System‑Cron anstoßen und deaktiviere die Ausführung pro Request. Das verhindert, dass Cron‑Jobs mit Session‑Operationen kollidieren.

Sicherheit, Lifetime und Nutzererlebnis

Längere Lifetimes halten Nutzer eingeloggt, steigern aber die Menge alter Sessions. Kürzere Werte senken die Last, können jedoch Anmeldungen früher beenden. Ich wähle daher Zeiträume, die Risiko und Komfort ausbalancieren, beispielsweise 30–60 Minuten im Admin und kürzer für anonyme Nutzer. Bei besonders sensiblen Inhalten setze ich strikte Modi und sichere Cookies gegen XSS und Transportfehler ab. So bleiben Daten geschützt, während die Performance verlässlich bleibt.

Nach dem Login rotiere ich Session‑IDs (session_regenerate_id(true)), um Fixation zu vermeiden, und nutze cookie_same_site, httponly und secure konsequent. In Single‑Sign‑On‑Szenarien plane ich die SameSite‑Wahl bewusst (Lax vs. None), damit das Nutzererlebnis stabil bleibt.

Cluster, Load-Balancer und Sticky Sessions

Wer mehrere App‑Server betreibt, sollte dateibasierte Sessions nur mit Sticky Sessions einsetzen, sonst verlieren Nutzer Zustände. Besser ist ein zentraler RAM‑Store. Ich prüfe die Latenz zwischen App und Store, stelle Timeouts knapp, aber nicht aggressiv ein, und plane ein Failover (z. B. Sentinel/Cluster bei Redis). Bei Wartungen ist wichtig, TTLs so zu wählen, dass ein kurzer Ausfall nicht sofort zu Massen‑Logouts führt.

Wirtschaftliche Abwägung und Migrationspfad

Ein Wechsel auf Redis oder Memcached kostet Betrieb, spart aber Zeit je Request und reduziert Supportfälle. Wer oft im Admin arbeitet, merkt den Unterschied sofort. Ich bewerte die Einsparungen durch schnellere Deployments, weniger Frust und weniger Abbrüche. Wenn Last steigt, plane ich die Migration früh statt spät, um Engpässe zu vermeiden. Eine klare Roadmap umfasst Tests, Rollout und Monitoring, bis die Latenz stabil und die GC‑Läufe unauffällig bleiben.

Für den Umstieg gehe ich schrittweise vor: In Staging aktiviere ich den RAM‑Store, fahre synthetische Last und prüfe p95/p99‑Latenzen. Dann rolle ich per Feature‑Flag in kleinen Prozenten aus und beobachte Fehlerquoten sowie Timeouts. Ein Rollback bleibt einfach, wenn ich session.name parallel variieren kann, sodass Sessions zwischen altem und neuem Backend nicht kollidieren. Wichtige Kennzahlen sind: Session‑Dateien pro Stunde (soll sinken), mediane TTFB (soll sinken), 5xx‑Rate (soll stabil bleiben) und Anteil Requests mit Session‑Lock über 100 ms (soll stark sinken).

Kurz zusammengefasst

Die php session gc verursacht Lags, weil zufällig gestartete Aufräumläufe lange Datei‑Operationen und Locks auslösen. Ich entschärfe das, indem ich die Wahrscheinlichkeit in Requests auf null setze, die Bereinigung per Cron plane und Sessions in RAM‑Stores lege. Hosting‑Ressourcen mit schnellem NVMe und ausreichendem RAM verringern die Blockade zusätzlich. WordPress profitiert spürbar, wenn Heartbeat gezügelt, Plugins gesichtet und unnötige Sessions vermieden werden. Wer diese Schritte beachtet, bringt Antwortzeiten runter, verhindert Blockierungen und hält die Admin-Oberfläche reaktionsfreudig, auch bei hohem Traffic.

Aktuelle Artikel