Eingeloggter Kunde, Produktseite, klickt auf „in den Warenkorb". Der Mini-Warenkorb-Spinner dreht sich vier Sekunden. Die Datenbank ist nicht überlastet. Redis ist nicht langsam. PHP hat freie Worker. Der Mini-Warenkorb-Refresh wartet in der Schlange hinter dem Add-to-Cart-Write, serialisiert von einem Session-Lock, von dem niemand im Team mehr weiß, dass er eingeschaltet ist.
Wenn du von Varnish-Hit-Rate unter 30 % auf Magento 2 diagnostizieren hierher gekommen sind: dieser Spinner ist die andere Hälfte des Problems. Es ist ein Zehn-Sekunden-Config-Flip, der auf Shops seit fünf Jahren ungefixt liegenbleibt, weil niemand nach dem initialen Setup die Session-Handler-Dokumentation liest. Die Defaults sind auf eine Welt getunt, in der Magento 2 nicht mehr lebt.
TL;DR
- Magentos Redis-Session-Handler hat
disable_locking=0als Default, was parallele Anfragen derselben Session hinter einem Redis-Level-Lock serialisiert. -
break_after_frontend=5heißt: jede Anfrage, die den Lock nicht bekommt, wartet bis zu fünf Sekunden, bevor sie aufgibt. Ein einziger Kunde mit drei AJAX-Calls in Flight kann sich selbst hinter seinem eigenen vorherigen Request einreihen. - Auf einem Varnish-gefronteten Storefront berührt cachbarer Gast-Traffic die Session fast nie. Der Lock leistet auf diesen Anfragen keine Arbeit und schadet denen, die die Session tatsächlich brauchen.
- Setz
disable_locking=1inapp/etc/env.php. Auf einem Shop, auf dem die Defaults jahrelang liefen, fällt die p95-TTFB auf eingeloggten Interaktionen spürbar. In Traces kollabiert derredis-session-read-Span von Sekunden auf Millisekunden. - Der eine Edge Case, der das ausschließt, ist ein Modul, das die Session als Serialisierungs-Queue nutzt. Das Muster ist selten, meistens falsch, und fast immer in fünf Minuten Grep auffindbar.
Was break_after_frontend=5 tatsächlich tut
Magento nutzt Colin Mollenhours php-redis-session-abstract als Redis-Session-Backend. Wenn eine Anfrage beginnt, die Session zu lesen, nimmt der Handler einen Redis-Level-Lock auf die Session-ID. Der Lock hat eine TTL (max_lifetime, Default 60 s), damit er nicht ewig hängenbleibt, falls PHP mitten im Request stirbt.
Wenn eine zweite Anfrage für dieselbe Session eintrifft, während die erste den Lock noch hält, landet sie in einer Wait-Loop. break_after_frontend ist das Warte-Budget in Sekunden für Storefront-Anfragen (nicht Admin). Der Default ist 5. Die zweite Anfrage dreht, prüft den Lock, schläft kurz, prüft erneut, bis entweder der Lock freigegeben oder das Budget ausgeschöpft ist.
Zwei Werte, die du in app/etc/env.php ebenfalls im Blick behalten solltest:
-
bot_first_lifetimeundbot_lifetimeregeln, wie lange Sessions für Bot-UAs leben. Halt das kurz. Eine Bot-Session, die herumliegt, ist ein Redis-Key, der dir Speicher kostet, ohne etwas zurückzugeben. -
max_concurrencybegrenzt, wie viele Anfragen gleichzeitig in der Wait-Loop sein dürfen. Default: 6. Wenn ein Bot oder ein fehlgeleitetes Skript auf eine einzelne Session hämmert, verhindert das, dass deine php-fpm-Worker komplett belegt werden.
Keiner dieser Knöpfe hilft gegen das eigentliche Problem: break_after_frontend=5 auf einem Storefront von 2026 ist eine selbstverursachte Fünf-Sekunden-Obergrenze auf die Hälfte deiner dynamischen Seitenladungen.
Warum ein Kunde sich selbst ausbremsen kann
Überleg, was passiert, wenn ein eingeloggter Kunde eine Produktseite auf einem modernen Magento-2-Storefront lädt.
Das HTML kommt aus Varnish, schnell. Dann feuert der Browser vier bis fünf fast gleichzeitige Requests ab, um die personalisierten Teile nachzuziehen: /customer/section/load/?sections=cart,messages,customer,directory-data für Mini-Warenkorb und Begrüßung; /rest/V1/carts/mine, falls ein Drittanbieter-Plugin den Warenkorb-Inhalt braucht; vielleicht einen Custom-REST-Endpoint für einen Promo-Banner; vielleicht einen Form-Key-Refresh; vielleicht ein Tracking-Pixel einer Ad-Plattform, das am Ende einen PHP-Endpoint anspricht.
Jede dieser Anfragen trägt dasselbe Session-Cookie. Jede dieser Anfragen will die Session lesen, um zu wissen, wer der Kunde ist. Mit den Defaults bekommt die erste den Lock. Die nächsten drei warten. Wenn die erste 400 ms zum Rendern braucht, sitzt die zweite 400 ms lang auf der Wait-Loop, bevor sie an der Reihe ist. Wenn die erste länger braucht (weil ein Catalog-Observer langsam ist oder ein Payment-Method-Plugin eine externe API ruft), wartet die zweite entsprechend länger.
Nichts in dieser Interaktion braucht Serialisierung. Jede Anfrage liest die Session, schreibt nicht, oder schreibt einen Teil, der nicht kollidiert. Aber der Lock weiß das nicht, und er ist von Natur aus pessimistisch.
Auf einem Shop, mit dem wir kürzlich gearbeitet haben, zeigte ein Production-Blackfire-Trace den Cm_RedisSession_Model_Session::read()-Span bei etwa zweieinhalb Sekunden auf einem Mini-Warenkorb-Request. Der Kunde hatte auf einer Produktseite „in den Warenkorb" geklickt, was die übliche AJAX-Kaskade ausgelöst hatte, und der Mini-Warenkorb-Refresh wartete am Ende auf den Add-to-Cart-Write. Datenbank untätig. Redis untätig. PHP untätig. Die Wait-Loop wartete einfach.
Warum FPC-gefrontete Shops das bedenkenlos umschalten können
Das Lehrbuch-Argument für Session Locking sind Race Conditions. Wenn zwei Anfragen derselben Session beide in $_SESSION schreiben, gewinnt der letzte Writer, und Daten gehen verloren. Locking verhindert das.
Dieses Argument war 2015 stärker als heute. Zwei Dinge haben sich geändert.
Erstens hält Magentos AJAX-Personalisierungs-Layer (section_data_provider, customer-data.js) die Server-Side-Session bewusst klein. Der schwere State, also Warenkorb-Inhalte, Kundengrüße und Messages, wird über REST-Endpoints bedient, die nicht darauf angewiesen sind, den Session-Write-Pfad zu blockieren. Auf einem halbwegs vanilla Magento-2-Build schreiben parallele AJAX-Calls gegen dieselbe Session selten denselben Key.
Zweitens: Full Page Cache (Varnish, Fastly, Cloudflare FPC) heißt, Gast-Traffic berührt die Session meistens gar nicht. Der Request landet auf Varnish, trifft ein gecachtes Objekt und kommt zurück, ohne PHP zu erreichen. Der Session-Handler ist gar nicht im Bild. Die Lock-Contention existiert nur auf dynamischen Anfragen, und das sind meistens die oben beschriebenen Gleich-Kunde-AJAX-Kaskaden, die von Locking nicht profitieren.
Die Behauptung, die hier verteidigt werden muss: Wenn ein Concurrent-Write-Race in einer Storefront-Session einen Bug auf deinem Shop verursacht, ist der Lock nicht dein Fix. Der Fix ist, den Code zu finden, der aus zwei gleichzeitigen Anfragen denselben Session-Key beschreibt, und ihn umzuschreiben, wahrscheinlich indem du den State komplett aus der Session herausnimmst. In der Praxis sind die einzigen Stellen, an denen Locking tatsächlich einen Bug verhindern würde, Custom-Module, die die Session wie eine Queue oder einen Counter behandeln. Diese Module sind mit oder ohne Lock kaputt.
Die Änderung, in einer Zeile
Öffne app/etc/env.php. Finde den session-Block, meist so:
1'session' => [ 2 'save' => 'redis', 3 'redis' => [ 4 'host' => '127.0.0.1', 5 'port' => '6379', 6 'database' => '3', 7 'password' => '', 8 'timeout' => '2.5', 9 'persistent_identifier' => '',10 'compression_threshold' => '2048',11 'compression_library' => 'gzip',12 'log_level' => '1',13 'max_concurrency' => '6',14 'break_after_frontend' => '5',15 'break_after_adminhtml' => '30',16 'first_lifetime' => '600',17 'bot_first_lifetime' => '60',18 'bot_lifetime' => '7200',19 'disable_locking' => '0',20 'min_lifetime' => '60',21 'max_lifetime' => '2592000',22 ],23],
Ändere disable_locking von '0' auf '1'. Config-Cache leeren (bin/magento cache:flush config), php-fpm neu starten, damit bestehende Worker die neue Config ziehen, fertig. Keine Datenbankänderung, kein Code-Deploy, keine Migration.
Admin-Session-Locking (break_after_adminhtml=30) ist eine andere Frage. Das Admin-Panel hat deutlich mehr Concurrent-Write-Contention als der Storefront, weil Admin-Sessions Permission-State, Store-Scope-State und Grid-Filter tragen, auf die mehrere Tabs fröhlich übereinander schreiben. Lass das adminhtml-Locking an, es sei denn, du weißt genau, was die Admins deines Shops tun.
Vorher und Nachher messen
Roll diese Änderung nicht aus, ohne sie zu messen. Ohne Zahl änderst du nur Config.
Der günstigste Blick ist ein Production-Blackfire- oder Tideways-Trace gegen einen eingeloggten Kunden-Klickpfad. Konkret: Produktseite laden, in den Warenkorb legen, den Mini-Warenkorb-Refresh im Wasserfall beobachten. Vor dem Flip ist der Session-Read-Span auf dem Mini-Warenkorb-Request lang und korreliert grob mit dem, was der Add-to-Cart gebraucht hat. Nach dem Flip fällt dieser Span auf einstellige Millisekunden. Wenn nicht, ist der Session-Handler gar nicht der Bottleneck auf diesem Pfad, und die Config-Änderung hilft dir nicht.
Wenn du kein Blackfire oder Tideways hast, kommst du mit XHProf auf einem Staging-Environment gegen eine geskriptete eingeloggte AJAX-Kaskade (ab oder hey mit Session-Cookie gegen /customer/section/load/ und eine Add-to-Cart-URL parallel) nah genug heran. Vergleich die p95-Response-Zeit der langsameren Anfrage über zehn Runs, vorher und nachher. Das Delta ist deine Antwort.
Production-Monitoring: Behalte die „PHP time"-Metrik für den customer/section/load-Endpoint in New Relic oder Datadog im Auge. Das ist das kanonische Frühwarnsignal für Session-Contention, weil er auf jeder Seite eingeloggter Kunden läuft und die Session meistens anfasst.
Wann du es nicht umschaltest
Es gibt ein Szenario, in dem disable_locking=1 die falsche Entscheidung ist: Ein Modul auf deinem Shop nutzt die Session als Queue oder Serialisierungs-Primitiv. Das Muster sieht so aus: das Modul liest einen Counter aus der Session, inkrementiert, schreibt zurück, und verlässt sich darauf, dass der Lock das Read-Modify-Write atomar macht. Ohne Lock lesen zwei parallele Anfragen denselben Wert, beide inkrementieren, beide schreiben denselben inkrementierten Wert, und du verlierst ein Increment.
Das Muster ist auf Magento 2 selten. Es ist auch immer falsch. Wenn du es findest, fix das Modul, halt den Lock nicht an, um es zu kaschieren. Ein Grep-Durchlauf bringt die meisten Täter zutage:
1grep -rn --include='*.php' -E '\$this->(checkoutSession|customerSession|session)->\w+\(.*\+' app/code/2grep -rn --include='*.php' 'setSession' app/code/ | grep -v Test
Der andere seltene Fall ist ein Custom-Checkout-Schritt, der explizit auf sequenziellen Session-Writes beruht (selten, aber wir haben es einmal in einem Loyalty-Points-Modul gesehen). Wenn dein Shop einen hat, merkst du es sofort, weil das Deaktivieren des Locks im Test einen kundenseitigen Bug auslöst. Das ist das Signal zum Zurücksetzen, nicht zum dauerhaften Anlassen des Locks als pauschalen Fix.
Weiterführende Artikel
- Varnish-Hit-Rate unter 30 % auf Magento 2 diagnostizieren, der Begleitartikel auf der Cache-Seite desselben Problems.
- Observer-Rekursion in Magento 2: die versteckten Kosten von sales_order_save_after, weil Session-Contention und Observer-Kaskaden sich oft auf demselben Checkout-Pfad potenzieren. Wenn du den Lock abschaltest und der Checkout immer noch langsam ist, ist die Kaskade wahrscheinlich das Nächste.
Die Redis-Session-Locking-Checkliste
-
01
Aktuelle Session-Config lesen
Öffne app/etc/env.php und finde den session.redis-Block. Notiere die aktuellen Werte von disable_locking, break_after_frontend, break_after_adminhtml und max_concurrency. Du brauchst die Baseline schriftlich, bevor du etwas änderst.
-
02
Bestätigen, dass Varnish Gast-Traffic tatsächlich fronted
curl -sI auf die Startseite. Cache-Control muss public mit einer max-age > 0 sein, Age sollte auf Wiederholungs-Requests steigen. Wenn Gast-HTML auf jeder Anfrage PHP trifft, fix das zuerst. disable_locking=1 ohne FPC ist eine aggressivere Änderung.
-
03
Vorher-Trace auf einem eingeloggten Pfad aufnehmen
Blackfire- oder Tideways-Trace von „PDP laden, in Warenkorb klicken" als eingeloggter Kunde. Notiere die Cm_RedisSession_Model_Session::read-Zeit und die Gesamt-Wall-Time der AJAX-Kaskade.
-
04
Nach Session-als-Queue-Anti-Patterns greppen
grep -rn --include='*.php' -E '\$this->(checkoutSession|customerSession|session)->\w+\(.*\+' app/code/. Jeder Treffer, der einen in der Session gespeicherten Counter inkrementiert, ist ein Risiko. Lesen. Die meisten Treffer sind harmlos, die schlechten sind leicht zu erkennen.
-
05
disable_locking auf 1 setzen
app/etc/env.php editieren, 'disable_locking' => '0' auf 'disable_locking' => '1' ändern. break_after_adminhtml unangetastet lassen. bin/magento cache:flush config laufen lassen und php-fpm neu starten, damit die Worker die neue Config laden.
-
06
Denselben Pfad erneut tracen
Gleicher Blackfire-/Tideways-Trace wie in Schritt 3. Der Session-Read-Span sollte von Sekunden auf einstellige Millisekunden fallen. Die AJAX-Kaskaden-Gesamt-Wall-Time sollte um das sinken, was die Wartezeit gekostet hat.
-
07
Production 24 Stunden beobachten
Behalte customer/section/load und Mini-Warenkorb-REST-Endpoints in New Relic oder Datadog im Auge. Bestellrate und Checkout-Conversion sollten stabil bleiben oder besser werden. Die Error Rate darf sich nicht bewegen.
-
08
Die Änderung dokumentieren
env.php mit einer Commit-Message committen, die das Warum erklärt. Dein zukünftiges Ich oder der nächste Entwickler, der diesen Shop anfasst, wird wissen wollen, dass disable_locking=1 eine bewusste Entscheidung war und kein versehentlich überschriebener Default.
Session-Contention ist selten das einzige, was einen Shop bremst
Wir fahren fokussierte Magento-2-Performance-Audits. Du bekommst eine Blackfire-Baseline, die konkreten Config- und Code-Änderungen, die etwas bewegen, und eine Prioritätenliste. Zwei bis drei Tage. Direkt am Code, keine 80-seitige PDF.