Zum Inhalt springen
Server & Hosting

Die Session-Sperre: wenn der Warenkorb den Shop ausbremst

Zwei gleichzeitige Anfragen desselben Besuchers laufen nacheinander, sobald beide die Sitzung öffnen. Woher die Wartezeit kommt und wie sie verschwindet.

14 Min. Lesezeit PHPSessionsBackendE-Commerce

Ein Shop, der unter Last langsam wird, ist ein bekanntes Bild. Schwerer zu erklären ist der andere Fall: Die Serverauslastung liegt nahe am Leerlauf, die Datenbank antwortet in wenigen Millisekunden, und trotzdem braucht der Aufruf des Warenkorb-Zählers eine halbe Sekunde - aber nur dann, wenn gleichzeitig eine andere Seite desselben Besuchers geladen wird. Die Ursache steht nicht im Profiler der Anwendung, sondern im Handbuch der Laufzeitumgebung: Solange ein Skript eine Sitzung geöffnet hat, ist sie für jedes andere Skript gesperrt. Dieser Beitrag zeigt, wie diese Reihenbildung entsteht, wo sie im Shop sichtbar wird, wie sich die Wartezeit je Anfrage messen lässt und mit welchen drei Eingriffen sie verschwindet.

Das Wichtigste in Kürze

  • Sitzungsdaten sind gesperrt, solange ein Skript sie geöffnet hat; laut Dokumentation darf zu jedem Zeitpunkt nur ein Skript auf einer Sitzung arbeiten (PHP-Handbuch). Zwei gleichzeitige Anfragen desselben Besuchers laufen deshalb nacheinander.
  • Der Befund ist am einzelnen Besucher sichtbar, nicht an der Serverlast. Erst wenn mehrere Aufrufe derselben Sitzung zeitlich nebeneinanderliegen, wird aus einem unauffälligen Mittelwert eine Wartezeit mit klarem Anfang und Ende.
  • Gemessen wird der Abstand zwischen dem Aufruf, der die Sitzung öffnen will, und dem Moment, in dem er sie bekommt. Ein Zeitwert im Antwortkopf macht diesen Anteil je Anfrage sichtbar, ohne die Anwendung umzubauen.
  • Der wirksamste Eingriff ist der frühe Abschluss: Ein Aufruf, der nur liest, gibt die Sitzung sofort zurück. Beim Start gibt es dafür eine Option, die die Sitzung unmittelbar nach dem Lesen schließt und unnötiges Sperren vermeidet (PHP-Handbuch).
  • Ein anderer Ablageort verkürzt die Wartezeit, hebt die Reihenbildung aber nicht auf. Wer sie auflösen will, muss die Sitzung früh schließen oder für Aufrufe, die keine brauchen, gar nicht erst starten.
  • Die Standardwerte gehören überprüft: 1.440 Sekunden Lebensdauer der Sitzungsdaten und 1 Prozent Wahrscheinlichkeit für den Aufräumlauf je Anfrage (PHP-Handbuch) sind Vorgaben, keine Entscheidungen.

Warum zwei Aufrufe nacheinander laufen

Beim Start einer Sitzung liest die Laufzeitumgebung die gespeicherten Daten und sperrt sie im selben Zug für andere Aufrufe. Die Dokumentation benennt den Grund unmissverständlich: Die Sitzungsdaten sind gesperrt, um gleichzeitige Schreibvorgänge zu verhindern, und darum darf zu jedem Zeitpunkt nur ein Skript auf einer Sitzung arbeiten (PHP-Handbuch). Freigegeben wird die Sperre erst, wenn das Skript endet oder sie ausdrücklich zurückgibt. Für einen einzelnen Seitenaufruf bleibt das folgenlos. Sobald ein Besucher aber mehrere Anfragen gleichzeitig stellt - und das tut jede zeitgemäße Oberfläche - wird aus paralleler Arbeit eine Warteschlange. Mit der Zahl der Arbeitsprozesse hat das nichts zu tun: Die freien Prozesse stehen bereit, sie kommen nur nicht an die Daten.

Entscheidend ist, dass die Sperre je Besucher gilt und nicht je Server. Zwei verschiedene Kunden stören einander nicht; zwei Registerkarten desselben Kunden dagegen schon. Genau deshalb fällt der Effekt in einem Lasttest mit tausend künstlichen Besuchern nicht auf: Dort hat jeder Testnutzer seine eigene Sitzung und stellt seine Anfragen brav hintereinander. Auf der Serverseite sieht alles gesund aus, während der echte Besucher wartet. Der Befund entsteht erst, wenn man die Aufrufe eines einzigen Besuchers zeitlich nebeneinanderlegt - mit Zeitstempel für Anfang und Ende, nicht nur mit einer Dauer.

Der Unterschied zur Kapazitätsfrage

Bei zu wenigen Arbeitsprozessen warten alle Besucher, und die Wartezeit steigt mit der Besucherzahl. Bei der Sitzungssperre wartet ein Besucher auf sich selbst, und die Wartezeit steigt mit der Zahl seiner gleichzeitigen Anfragen. Beide Bilder sehen in einer Übersicht gleich aus - langsame Antworten -, verlangen aber gegensätzliche Maßnahmen. Mehr Arbeitsprozesse helfen im zweiten Fall nicht, sie verschieben die Wartezeit nur an eine andere Stelle.

Wo die Wartezeit im Shop auftaucht

Am deutlichsten trifft es die kleinen Nachladungen, die neben der eigentlichen Seite laufen. Der Warenkorb-Zähler im Kopfbereich, die Prüfung der Verfügbarkeit auf der Produktseite, das Nachladen von Filterergebnissen, der Vergleichsmerker, der Hinweis auf zuletzt gesehene Artikel: Jede dieser Anfragen öffnet die Sitzung, weil sie die Warenkorb-Kennung braucht - oder weil ein Rahmenwerk sie unbesehen für jede Anfrage öffnet. Solange die eigentliche Seite noch erzeugt wird, stehen alle diese Anfragen still. Im Wasserfall des Entwicklerwerkzeugs sieht das aus wie ein langsamer Endpunkt, und genau so wird es meist auch behandelt. Besonders teuer wird die Reihenbildung im Bestellweg, wo mehrere Schritte kurz hintereinander in die Sitzung schreiben.

Ein zweites Muster betrifft langlaufende Anfragen. Ein Export, eine Bildumwandlung, ein Abruf bei einem Zahlungsdienst - alles, was Sekunden statt Millisekunden braucht - blockiert währenddessen jede weitere Anfrage desselben Besuchers, auch das bloße Weiterklicken. In Shopware-Shops fällt das häufig bei Importen im Kundenkonto auf, in anderen Systemen beim Erzeugen einer Rechnung als PDF. Der Besucher klickt, sieht nichts, klickt erneut - und erzeugt damit eine weitere Anfrage, die sich hinten anstellt. Aus einer langsamen Aktion werden so drei.

Ob eine Anfrage die Sitzung wirklich braucht, lässt sich fast immer beantworten, bevor die erste Zeile Code entsteht. Die folgende Gegenüberstellung zeigt die Fälle, die in einem Shop den Großteil der Aufrufe ausmachen.

Art der AnfrageBraucht die SitzungRichtiger Umgang
Katalogseite ohne persönliche Bausteinenicht enthalten Sitzung gar nicht erst starten
Warenkorb-Zähler, zuletzt gesehene Artikelnur lesendlesen und sofort schließen
Filter- und Suchergebnis nachladenmeist neinKennung aus dem Aufruf statt aus der Sitzung
Artikel in den Warenkorb legenschreibendöffnen, schreiben, sofort schließen
Bestellschritt absendenschreibendkurz halten, lange Arbeit auslagern
Export oder Rechnung erzeugennicht enthalten Warteschlange, Fortschritt ohne Sitzung abfragen

Die dritte Zeile ist die unauffälligste und zugleich die häufigste. Viele Nachladungen öffnen die Sitzung nur deshalb, weil im gemeinsamen Vorlauf der Anwendung ein Sitzungsstart steht. Dieselbe Gewohnheit kostet an anderer Stelle die Trefferquote des Caches: Wo eine Sitzung startet, entsteht ein Cookie, und wo ein Cookie entsteht, wird die Antwort persönlich. Wer den Sitzungsstart aus dem Vorlauf herausnimmt und an die Stellen setzt, die ihn wirklich brauchen, gewinnt beides zurück.

Die Sperre messen, nicht vermuten

Die Messung ist einfacher, als sie klingt, weil die Wartezeit einen klaren Anfang und ein klares Ende hat: Sie liegt zwischen dem Aufruf, der die Sitzung öffnen will, und dem Moment, in dem er sie bekommt. Wer diesen Abstand misst und als Zeitwert an die Antwort hängt, sieht ihn anschließend im Entwicklerwerkzeug neben allen anderen Abschnitten. Der Antwortkopf für Serverzeiten ist dafür der kürzeste Weg, weil er ohne eigene Auswertung auskommt. Parallel dazu gehört derselbe Wert ins Protokoll, damit er sich über Tage auswerten lässt.

sitzungssperre-messen.php
<?php
// Wartezeit auf die Sitzungssperre je Anfrage messen
$start  = hrtime(true);
session_start();                       // blockiert, solange ein anderer Aufruf schreibt
$sperre = (hrtime(true) - $start) / 1e6;   // Millisekunden

// Aufrufe, die nichts in die Sitzung schreiben, geben sie sofort zurück
if (!$brauchtSchreibzugriff) {
    session_write_close();
}

// Im Entwicklerwerkzeug neben allen anderen Abschnitten sichtbar
header('Server-Timing: sesslock;desc="Sitzungssperre";dur=' . round($sperre, 1));

register_shutdown_function(static function () use ($start, $sperre): void {
    $gesamt = (hrtime(true) - $start) / 1e6;
    error_log(json_encode([
        'pfad'      => $_SERVER['REQUEST_URI'] ?? '',
        'sitzung'   => substr(session_id() ?: '-', 0, 8),   // nur zum Gruppieren
        'sperre_ms' => round($sperre, 1),
        'gesamt_ms' => round($gesamt, 1),
        'anteil'    => $gesamt > 0 ? round($sperre / $gesamt * 100) : 0,
    ], JSON_UNESCAPED_SLASHES));
});

Drei Zahlen reichen für die Einordnung: die Wartezeit auf die Sperre, die Gesamtdauer der Anfrage und der Anteil der ersten an der zweiten. Liegt der Anteil bei einzelnen Aufrufen über der Hälfte, ist die Reihenbildung das Hauptproblem dieser Anfrage - unabhängig davon, wie gut die Antwortzeit des Servers im Mittel aussieht. Aussagekräftig wird die Reihe erst über mehrere Tage, weil die Sperre nur dann auftritt, wenn sich zwei Anfragen tatsächlich überschneiden.

Wartezeit je Anfrage

Der Abstand zwischen dem Öffnen der Sitzung und dem Erhalt der Sperre. Alles andere leitet sich aus dieser einen Zahl ab.

Sitzungskennung im Protokoll

Nur die ersten Stellen der Kennung mitschreiben. Das reicht, um Anfragen desselben Besuchers zu gruppieren, und hält den Personenbezug gering.

Überschneidung sichtbar machen

Zwei Anfragen mit derselben Kennung und überlappenden Zeitfenstern sind der Beleg. Ohne Zeitstempel für Anfang und Ende bleibt es eine Vermutung.

Lesend oder schreibend

Je Endpunkt festhalten, ob er die Sitzung verändert hat. Aufrufe, die nur lesen, sind die erste Gruppe für den frühen Abschluss.

Ablageort und Speicherzeit

Wie lange das Lesen und Schreiben der Sitzungsdaten selbst dauert, gehört getrennt gemessen - sonst verschwimmt Wartezeit mit Speicherzeit.

Anteil an der Gesamtdauer

Ein Verhältnis statt eines absoluten Werts macht Endpunkte vergleichbar, die von Haus aus unterschiedlich lange brauchen.

Die Reihenfolge, die sich bewährt hat

Erst messen, dann die lesenden Aufrufe früh schließen, dann den Sitzungsstart aus dem gemeinsamen Vorlauf nehmen, dann über den Ablageort entscheiden. Wer mit dem letzten Schritt beginnt, verkürzt die Wartezeit, statt sie zu entfernen - und hat die Messung, die den Gewinn belegen würde, noch nicht.

Die Sitzung früh schließen

Der frühe Abschluss ist ein Einzeiler und trotzdem der wirksamste Eingriff. Nach dem Lesen der Sitzungsdaten gibt session_write_close() die Sperre zurück; die Daten stehen im Speicher weiter zur Verfügung, nur Änderungen landen nicht mehr im Ablageort. Für Aufrufe, die von vornherein nichts schreiben, gibt es den kürzeren Weg direkt beim Start: Die Dokumentation beschreibt eine Option, mit der die Sitzung unmittelbar nach dem Lesen geschlossen wird und unnötiges Sperren dadurch entfällt (PHP-Handbuch). Beides zusammen nimmt den Nachladungen ihre Wartezeit und wirkt direkt auf die Reaktionszeit auf Eingaben, weil genau diese Anfragen an einem Klick hängen.

Die Gefahr liegt nicht im Schließen, sondern im späteren Schreiben. Wer nach dem Abschluss noch etwas in die Sitzung legt, verliert diese Änderung stillschweigend - eine Fehlerklasse, die im Entwicklungsbetrieb selten auffällt, weil dort selten zwei Anfragen kollidieren. Zwei Gewohnheiten halten das im Griff: den Abschluss so weit wie möglich nach oben ziehen, also direkt hinter das Lesen, und jeden Schreibzugriff auf die Sitzung über eine einzige Stelle im Code führen, die prüft, ob die Sitzung überhaupt noch offen ist. Wo ein Rahmenwerk die Sitzung selbst verwaltet, gehört derselbe Schalter in dessen Konfiguration statt in den Anwendungscode.

  • Aufrufe, die nur lesen, über die Startoption schließen lassen statt über einen nachträglichen Aufruf - das spart den Umweg über das Öffnen der Sperre.
  • Den Abschluss vor jeden Abruf bei einem Fremdsystem setzen; eine Zeitüberschreitung dort darf nicht die ganze Sitzung des Besuchers blockieren.
  • Lange Arbeit - Export, PDF, Bildumwandlung - hinter eine Warteschlange legen und den Fortschritt ohne Sitzung abfragen.
  • Den Sitzungsstart aus dem gemeinsamen Vorlauf nehmen und in die Bausteine setzen, die persönliche Daten zeigen.
  • Kennungen, die nur der Wiedererkennung dienen, im Aufruf mitgeben statt aus der Sitzung zu lesen.
  • Nach jedem Umbau prüfen, ob eine Anmeldung, ein Warenkorb und ein Bestellabschluss weiterhin funktionieren - die Sitzung ist der empfindlichste Teil des Shops.

Was im Entwicklungsbetrieb nie auftritt

Auf einem Entwicklungsrechner arbeitet ein Mensch mit einer Registerkarte. Dort kollidieren zwei Anfragen fast nie, und deshalb verschwindet die Wartezeit aus dem Blickfeld, bevor sie jemand gesehen hat. Reproduzierbar wird sie erst, wenn man zwei Aufrufe derselben Sitzung absichtlich überlappen lässt: einen künstlich verzögerten Seitenaufruf und daneben den Warenkorb-Zähler. Was dann an Wartezeit erscheint, ist keine Laborkuriosität, sondern der Alltag jedes Besuchers, der die Oberfläche zügig bedient.

Ablageort und Startpunkt der Sitzung

In der Voreinstellung liegen die Sitzungsdaten im Dateisystem; die Dokumentation nennt dafür den Dateihandler als Standard (PHP-Handbuch). Auf einem Server mit langsamem Speicher oder einem geteilten Netzlaufwerk kostet jedes Öffnen und Schließen zusätzliche Zeit, und diese Zeit verlängert die Sperre für alle wartenden Anfragen. Ein Ablageort im Arbeitsspeicher verkürzt diesen Anteil spürbar. Er hebt die Reihenbildung aber nicht auf: Auch dort sperrt die Sitzung, nur eben kürzer. Das gilt ebenso für die übrigen Caching-Schichten - sie beschleunigen Arbeit, die trotzdem nacheinander stattfindet.

Zur Konfiguration gehören drei Werte, die oft unbesehen übernommen werden. Die Lebensdauer der Sitzungsdaten liegt in der Voreinstellung bei 1.440 Sekunden, also 24 Minuten (PHP-Handbuch); wer länger lebende Warenkörbe erwartet, muss sie bewusst erhöhen, statt sich über verlorene Körbe zu wundern. Der Aufräumlauf startet in der Voreinstellung mit einer Wahrscheinlichkeit von 1 Prozent je Anfrage (PHP-Handbuch) - auf einem Server mit vielen kleinen Sitzungsdateien ist das ein gelegentlicher Ausreißer in der Antwortzeit, den man sonst den Datenbankabfragen zuschreibt. Und die dritte Vorgabe arbeitet bereits für Sie: Sitzungsdaten werden nur dann neu geschrieben, wenn sie sich geändert haben (PHP-Handbuch). Das spart Schreibzeit, nicht aber die Sperre.

session.ini
; Ablageort: Arbeitsspeicher statt Dateisystem verkürzt die Sperre,
; er hebt die Reihenbildung aber nicht auf.
session.save_handler = redis
session.save_path    = "tcp://127.0.0.1:6379?database=2"

; Lebensdauer und Aufräumlauf bewusst setzen statt erben
session.gc_maxlifetime = 1440          ; Vorgabe: 1440 Sekunden
session.gc_probability = 1             ; 1/100 = 1 Prozent je Anfrage (PHP-Handbuch)
session.gc_divisor     = 100

; Schreibt nur zurück, wenn sich etwas geändert hat
session.lazy_write = 1

; Keine Sitzung ohne Anlass: der Start gehört in den Baustein, nicht in den Vorlauf
session.auto_start = 0

Vom Befund zur Abnahme

Ein Befund taugt zur Abnahme, wenn er vor und nach dem Eingriff mit derselben Messung belegt ist: dieselben Endpunkte, dasselbe Zeitfenster, dieselbe Auswertung nach Vorlagen statt Einzeladressen. Als Zielwerte taugen die veröffentlichten Schwellen. Die Reaktionszeit auf Eingaben gilt bis 200 Millisekunden als gut (web.dev), für die Serverantwortzeit werden 0,8 Sekunden oder weniger als Richtwert genannt (web.dev), und gemessen wird jeweils am 75. Perzentil der Aufrufe, getrennt nach mobilen und stationären Geräten (web.dev). Eine Nachladung, deren halbe Laufzeit Wartezeit auf eine Sperre ist, verfehlt beide Werte, ohne dass eine Zeile Anwendungscode langsam wäre.

  1. Wartezeit auf die Sperre je Anfrage erfassen und zwei Wochen laufen lassen, bevor etwas geändert wird.
  2. Endpunkte danach sortieren, wie oft sie warten - nicht danach, wie lange sie im Mittel brauchen.
  3. Lesende Aufrufe auf den frühen Abschluss umstellen und die Messung unverändert weiterlaufen lassen.
  4. Den Sitzungsstart aus dem gemeinsamen Vorlauf nehmen; jeden Baustein einzeln nachziehen statt alle auf einmal.
  5. Erst danach über den Ablageort entscheiden, mit der Messung als Beleg für den Gewinn.
  6. Den Vorher-Nachher-Vergleich je Endpunkt festhalten; er ist der Teil, den eine Abnahme braucht.

Der häufigste Befund in solchen Messungen ist kein einzelner langsamer Endpunkt, sondern eine kleine Nachladung, die auf jeder Seite mitläuft und in einem Teil der Fälle wartet. Für Online-Shops ist das typischerweise der Warenkorb-Zähler: technisch belanglos, in der Summe aber die teuerste Anfrage des Bestands. Wer ihn als Erstes umstellt, hat den sichtbarsten Teil der Arbeit an einem Vormittag erledigt.

Die Sitzungssperre ist der seltene Fall, in dem mehr Hardware nichts bringt. Es gibt nichts zu beschleunigen - es gibt nur etwas freizugeben.

Projekterfahrung aus Performance-Projekten für Shops und Kundenportale

Der Aufwand hält sich in Grenzen: eine Messung einbauen, die lesenden Aufrufe umstellen, den Sitzungsstart sortieren. Was daran Zeit kostet, ist nicht der Eingriff, sondern die Suche nach den Stellen, an denen die Sitzung unbemerkt geöffnet wird - in eigenen Erweiterungen, in Vorlagen, in Zwischenschichten. Genau dafür ist die Messung da, und genau deshalb steht sie am Anfang und nicht am Ende einer technischen Analyse.

Dieser Artikel stützt sich auf: PHP-Handbuch (session_write_close, Session-Konfiguration, session_start) und web.dev (Schwellenwerte für Reaktionszeit und Serverantwortzeit, Auswertung am 75. Perzentil).

Verwandte Artikel

Server & Hosting

PHP-Laufzeit optimieren: OPcache, JIT und FPM-Worker

Zwischen Webserver und Datenbank liegt die PHP-Laufzeit. Wie OPcache, Preloading, JIT und die FPM-Worker die Antwortzeit unter Last bestimmen.

12 Min. Lesezeit
Shops & CMS

Checkout-Tempo: die letzten Sekunden vor der Bestellung

Der Checkout lässt sich nicht wegcachen: Warum die fünf Schritte vor der Bestellung eigene Tempo-Budgets brauchen und wo im Shop Sie messen sollten.

15 Min. Lesezeit
Server & Hosting

Server-Timing: wo die Zeit im Backend wirklich bleibt

Der TTFB nennt nur die Summe. Mit dem Server-Timing-Header zerlegen Sie die Serverzeit im Browser in Datenbank, Zwischenspeicher, Anwendung und Vorlage.

13 Min. Lesezeit