Logo run_as_root - Magento B2B Agency Würzburg
PERFORMANCE

Redis Session Locking in Magento 2: When disable_locking=1 Saves Your Checkout

Magento's default Redis session locking serialises concurrent AJAX from the same customer. When and why to flip disable_locking=1 on FPC-fronted stores.


On this page

    Signed-in customer, product page, clicks add-to-cart. The mini-cart spinner spins for four seconds. The database isn't overloaded. Redis isn't slow. PHP has spare workers. The mini-cart refresh is waiting in line behind the add-to-cart write, serialised by a session lock nobody on the team remembers turning on.

    If you got here from Diagnosing a Sub-30% Varnish Hit Rate in Magento 2, this spinner is the other half of the problem. It's a ten-second config flip that's been sitting unfixed on stores for five years, because nobody reads the session handler docs after the initial install. The defaults are tuned for a world that Magento 2 no longer lives in.

    TL;DR

    • Magento's Redis session handler defaults to disable_locking=0, which serialises concurrent requests from the same session behind a Redis-level lock.
    • break_after_frontend=5 means any request that can't get the lock waits up to five seconds before giving up. A single customer with three AJAX calls in flight can queue themselves behind their own previous request.
    • On a Varnish-fronted storefront, cacheable guest traffic almost never touches the session. The lock is doing nothing useful on those requests and hurting the ones that actually need the session.
    • Flip disable_locking=1 in app/etc/env.php. On a store where the defaults have been running for years, expect p95 TTFB on logged-in interactions to drop noticeably. In traces, the redis-session-read span collapses from seconds to milliseconds.
    • The one edge case that rules this out is a module that treats the session as a serialisation queue. That pattern is rare, usually wrong, and almost always findable in five minutes of grep.

    What break_after_frontend=5 actually does

    Magento uses Colin Mollenhour's php-redis-session-abstract as its Redis session backend. When a request starts reading the session, the handler takes a Redis-level lock keyed on the session ID. The lock has a TTL (max_lifetime, 60s by default) so it can't hang forever if PHP dies mid-request.

    If a second request for the same session arrives while the first still holds the lock, it enters a wait loop. break_after_frontend is the wait budget, in seconds, for storefront (non-admin) requests. The default is 5. The second request spins, checks the lock, sleeps briefly, checks again, until either the lock is released or the budget is exhausted.

    Two numbers worth noting while you're in app/etc/env.php:

    • bot_first_lifetime and bot_lifetime govern how long sessions live for UA strings that match the bot pattern. Keep these short. A bot session that hangs around is a Redis key that costs you memory for no return.
    • max_concurrency caps how many requests can be in the wait loop at once. Defaults to 6. If you've got a bot or a misbehaving script hammering a single session, this is what prevents it from exhausting php-fpm workers entirely.

    None of those knobs help with the real issue: break_after_frontend=5 in a 2026 storefront is a self-inflicted five-second ceiling on half your dynamic page loads.

    Why one customer can stall themselves

    Think about what happens when a signed-in customer loads a product page on a modern Magento 2 storefront.

    The HTML lands from Varnish, fast. Then the browser fires four or five near-simultaneous requests to fill in the personalised bits: /customer/section/load/?sections=cart,messages,customer,directory-data for the mini-cart and greeting; /rest/V1/carts/mine if a third-party plugin wants cart contents; maybe a custom REST endpoint for a promo banner; maybe a form-key refresh; maybe an ad platform's tracking pixel that ends up invoking a PHP endpoint.

    Every one of those requests carries the same session cookie. Every one of those requests wants to read the session to know who the customer is. Under the default settings, the first request in takes the lock. The next three wait. If the first request takes 400ms to render, the second request sits there doing nothing for 400ms before it gets its turn. If the first request takes longer, say because a catalog observer is slow or a payment-method plugin is calling an external API, the second request sits longer.

    Nothing in this interaction needs serialisation. Each request is reading the session, not writing it, or writing a part of it that doesn't conflict. But the lock doesn't know that, and it's pessimistic by design.

    On a store we worked on recently, a production Blackfire trace showed the Cm_RedisSession_Model_Session::read() span sitting at around two and a half seconds on a mini-cart request. The customer had clicked "add to cart" on a product page, which kicked off the usual AJAX cascade, and the mini-cart refresh ended up waiting for the add-to-cart write to complete. Database idle. Redis idle. PHP idle. The wait loop was just waiting.

    ⚠️
    The mistake most people make is looking at this trace and blaming Redis. Redis is fine. Redis is a microsecond-latency key-value store doing exactly what it's told. The bottleneck is the application-layer lock on top of Redis, and it lives in PHP, not in Redis.

    Why FPC-fronted stores are safe to flip

    The textbook argument for session locking is race conditions. If two requests for the same session both write to $_SESSION, the last writer wins and data gets lost. Locking prevents that.

    That argument was stronger in 2015 than today. Two things changed.

    First, Magento 2's AJAX personalisation layer (section_data_provider, customer-data.js) deliberately keeps the server-side session small. The heavy state, cart contents, customer greeting, messages, is served through REST endpoints that don't rely on blocking the session-write path. On a reasonably vanilla Magento 2 build, concurrent AJAX against the same session rarely writes the same key.

    Second, full page cache (Varnish, Fastly, Cloudflare FPC) means guest traffic mostly doesn't touch the session at all. The request lands on Varnish, hits a cached object, and returns without ever reaching PHP. The session handler isn't in the picture. The lock contention problem only exists on the dynamic requests, and those are mostly the same-customer AJAX cascade described above, which doesn't benefit from locking.

    The specific claim worth making: if a concurrent-write race in a storefront session causes a bug on your store, locking is not your fix. The fix is to find the code that's writing the same session key from two simultaneous requests and rewrite it to not do that, probably by moving the state out of the session entirely. In practice the only places we've seen where locking would actually prevent a bug are custom modules doing ill-advised things like treating the session as a queue or a counter. Those modules are broken with or without the lock.

    The change, in one line

    Open app/etc/env.php. Find the session block, which usually looks like this:

    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],

    Flip disable_locking from '0' to '1'. Clear config cache (bin/magento cache:flush config), restart php-fpm so existing workers pick up the new config, and you're done. No database change, no code deploy, no migration.

    Admin-area session locking (break_after_adminhtml=30) is a separate question. The admin panel has a lot more concurrent-write contention than the storefront, because admin sessions carry permission state, store-scope state, and grid filters that multiple tabs happily stomp on. Leave adminhtml locking on unless you know exactly what the admins on your store are doing.

    Measuring the before and after

    Don't ship this change without a measurement. Without a number, you're just changing config.

    The cheapest read is a production Blackfire or Tideways trace against a logged-in customer click path. Specifically: load a product page, add to cart, watch the mini-cart refresh in the waterfall. Before the flip, the session-read span on the mini-cart request is long and roughly correlates with whatever the add-to-cart request took. After the flip, that span drops to single-digit milliseconds. If it doesn't, the session handler isn't actually the bottleneck on that path and the config change isn't going to help you.

    If you don't have Blackfire or Tideways, XHProf on a staging environment against a scripted logged-in AJAX cascade (ab or hey with a session cookie, hitting /customer/section/load/ and an add-to-cart URL concurrently) gets you close enough. Compare p95 response time of the slower request across ten runs, before and after. The delta is your answer.

    Production monitoring: watch your New Relic or Datadog "PHP time" metric for the customer/section/load endpoint. That one's the canonical early warning for session contention because it runs on every page for logged-in customers and touches the session on most of them.

    When not to flip it

    There is one scenario where disable_locking=1 is the wrong call: a module on your store treats the session as a queue or serialisation primitive. The pattern looks like this: the module reads a counter out of the session, increments it, writes it back, and relies on the lock to make the read-modify-write atomic. Without the lock, two concurrent requests both read the same value, both increment, both write the same incremented value, and you lose an increment.

    That pattern is rare on Magento 2. It's also always wrong. If you find it, fix the module, don't keep locking on to paper over it. A grep pass surfaces most offenders quickly:

    1grep -rn --include='*.php' -E '\$this->(checkoutSession|customerSession|session)->\w+\(.*\+' app/code/
    2grep -rn --include='*.php' 'setSession' app/code/ | grep -v Test

    The other rare case is a custom checkout step that explicitly depends on sequential session writes (rare, but we have seen it once in a loyalty-points module). If your store has one, you'll know because disabling the lock will immediately cause a customer-facing bug in test. That's the signal to revert, not to keep locking on as a general fix.

    ⚠️
    If you flip the lock off and a test order fails, do not flip the lock back on and walk away. The module causing the failure is broken regardless of lock semantics, and the lock is masking the bug. Fix the module.

    The Redis session locking checklist

    1. Read your current session config

      Open app/etc/env.php and find the session.redis block. Note the current values of disable_locking, break_after_frontend, break_after_adminhtml, and max_concurrency. You want the baseline in writing before you change anything.

    2. Confirm Varnish is actually fronting guest traffic

      curl -sI the homepage. Cache-Control should be public with a non-zero max-age, Age should increment on repeat requests. If guest HTML is hitting PHP on every request, fix that first. disable_locking=1 without FPC is a more aggressive change.

    3. Capture a before-trace on a logged-in path

      Blackfire or Tideways trace of "load PDP, click add to cart" while signed in. Record the Cm_RedisSession_Model_Session::read time and the total wall time of the AJAX cascade.

    4. Grep for session-as-queue anti-patterns

      grep -rn --include='*.php' -E '\$this->(checkoutSession|customerSession|session)->\w+\(.*\+' app/code/. Any hit that increments a counter stored in the session is a risk. Read it. Most hits are benign, but the bad ones are easy to spot.

    5. Flip disable_locking to 1

      Edit app/etc/env.php, change 'disable_locking' => '0' to 'disable_locking' => '1'. Leave break_after_adminhtml alone. Run bin/magento cache:flush config and restart php-fpm so worker processes reload.

    6. Re-trace the same path

      Same Blackfire/Tideways trace as step 3. Session-read span should collapse from seconds to single-digit milliseconds. AJAX cascade total wall time should drop by whatever the waiting used to cost.

    7. Watch production for 24 hours

      Keep an eye on customer/section/load and minicart REST endpoints in New Relic or Datadog. Order placement rate and checkout conversion should be stable or better. Error rate should not move.

    8. Document the change

      Commit env.php to version control with a message explaining why. Future-you, or the next dev to touch this store, will want to know that disable_locking=1 was a deliberate choice and not a default that got accidentally overwritten.

    Session contention is rarely the only thing slowing a store down

    We run focused Magento 2 performance audits. You get a Blackfire baseline, the exact config and code changes that move the needle, and a priority list. Two to three days. Straight to code, no 80-page PDF.

    Book a performance audit