avoiding locking and Ajax stalls
A misconfigured session backend is one of the most common yet hardest to diagnose causes of slow Magento shops. Session locking, timeout values and the break_after_frontend parameter decide whether parallel Ajax requests within a session block each other or run cleanly side by side. This article shows step by step how to correctly configure the Redis session backend.
Table of Contents
- 1. Why Redis makes sense as a session backend
- 2. env.php: basic session backend configuration
- 3. Understanding session locking
- 4. break_after_frontend and break_after_adminhtml in detail
- 5. Timeout tuning: gc_maxlifetime versus Redis TTL
- 6. Avoiding session related slowdowns
- 7. min_lifetime, max_lifetime and session cleanup
- 8. Monitoring: analyzing session keys with redis-cli
- 9. Locking strategies compared
- 10. Summary
- 11. FAQ
1. Why Redis makes sense as a session backend
By default, Magento stores session data as files under var/session. On a single server setup this works fine, but as soon as several web servers sit behind a load balancer, every session needs to be available on every server, otherwise a customer gets unexpectedly logged out whenever a request lands on a different server. A central session backend with Redis solves this because all web servers share the same source of session data.
Beyond pure availability, a Redis session backend also brings performance benefits: reading and writing session data sits in the sub millisecond range, while file based sessions are slowed down under high concurrent load by file system locking. Magento uses the class Magento\Framework\Session\SaveHandler\Redis for Redis sessions, built on the Composer package colinmollenhour/php-redis-session-abstract.
The key difference from the cache backend is that session data is per user, relatively short lived, but read and written very frequently, especially in Ajax heavy frontend interactions such as cart updates or wishlist actions. This is exactly where tuning of the session backend comes in, which the rest of this article covers in detail.
2. env.php: basic session backend configuration
Basic configuration of the session backend happens under the session key in app/etc/env.php, separate from the cache section. The save parameter must be set to redis, while redis as a sub array holds server, port, database index and the behavior parameters explained throughout this article.
It is important to choose a dedicated database index for the session backend, separate from the cache backend and the full page cache. Session data has a different growth pattern than cache entries, especially during traffic spikes with many new, not logged in visitors, each creating their own session.
<?php
// app/etc/env.php - session section, Redis as session backend
return [
// ... other env.php keys omitted for brevity
'session' => [
'save' => 'redis',
'redis' => [
'host' => '127.0.0.1',
'port' => '6379',
'password' => '',
'timeout' => '2.5',
'persistent_identifier' => '',
'database' => '2',
'compression_threshold' => '2048',
'compression_library' => 'gzip',
'log_level' => '1',
'max_concurrency' => '6',
'break_after_frontend' => '5',
'break_after_adminhtml' => '30',
'first_lifetime' => '600',
'bot_first_lifetime' => '60',
'bot_lifetime' => '7200',
'disable_locking' => '0',
'min_lifetime' => '60',
'max_lifetime' => '2592000',
],
],
];
3. Understanding session locking
PHP sessions are exclusively locked by default: as long as a request holds a session open, every other parallel request for the same session has to wait until the first request closes it. Magento's session backend follows this behavior by default to prevent race conditions during concurrent writes, for instance when two Ajax requests modify the cart at the same time.
The disable_locking parameter can turn this behavior off entirely, which is risky: without locking, competing writes can overwrite session data inconsistently, so a cart item might get lost. Instead of enabling disable_locking, the recommended strategy is to fine tune locking so it only kicks in where it is actually needed. That is exactly what the break_after_* parameters in the next section accomplish.
max_concurrency limits how many parallel requests may access the same session at most before further requests get rejected. Setting it too low leads to HTTP 503 errors in Ajax heavy themes, while setting it too high can, with faulty frontend code, lead to a cascade of parallel lock attempts that slow the session backend down under load.
# Check the currently active locking-related settings for the session backend
bin/magento config:show session | grep -E "disable_locking|max_concurrency|break_after"
4. break_after_frontend and break_after_adminhtml in detail
break_after_frontend specifies, in seconds, after how long a waiting request forcibly breaks the session lock instead of waiting indefinitely. Magento's default value of 5 seconds makes sense for most shops: a request waiting more than 5 seconds for a locked session usually points to a stuck preceding request, for example a slow external API call within the same session context.
break_after_adminhtml controls the same behavior for the admin area, with a higher default of 30 seconds, because admin operations such as product imports or reindexing can take longer, and breaking the lock too early there would lead to inconsistent changes. The session backend therefore deliberately distinguishes between frontend and admin context, because locking patience requirements are fundamentally different.
A common tuning mistake is setting break_after_frontend too low, for instance to 1 second, hoping to reduce wait times. This causes locks to be broken too early, which in turn produces exactly the race conditions locking is meant to prevent. The right lever against long wait times is not shortening the break timeout, but eliminating the root cause: slow, session blocking controller actions.
# Find frontend actions that keep sessions locked too long (example log grep)
grep "session lock" var/log/system.log | tail -50
# Check current session backend settings applied at runtime
bin/magento config:show session
# Inspect an active session lock key directly in Redis
redis-cli -n 2 KEYS "*_SessionLock*"
redis-cli -n 2 TTL "sess_locked_key"
5. Timeout tuning: gc_maxlifetime versus Redis TTL
PHP's classic session.gc_maxlifetime in php.ini defines how long a session stays valid without activity before the garbage collector removes it. With the Redis session backend, the TTL of the respective Redis key takes over this role instead, controlled via first_lifetime, min_lifetime and max_lifetime. The session.gc_maxlifetime setting in php.ini is still read, but the actual expiry control happens through Redis itself.
first_lifetime sets the initial lifetime of a new session in seconds, default 600. For logged in customers, Magento automatically extends the lifetime with every activity. For bots and crawlers, detected through user agent heuristics, separate, shorter values apply via bot_first_lifetime and bot_lifetime, which prevents mass bot requests from unnecessarily filling the session backend with short lived but numerous sessions.
6. Avoiding session related slowdowns
The most common cause of a Magento shop feeling slow is not the session backend itself, but controller code that keeps the session open longer than necessary. Every session write operation, for example $session->setData(...), implicitly holds the lock until the request finishes or session_write_close() is called. Long running actions such as PDF generation or external API calls inside a controller therefore block every parallel request for the same session, for instance Ajax calls updating the mini cart.
A proven pattern is to explicitly call session_write_close() in controllers that no longer need to write to the session, as soon as all necessary data has been written. This releases the lock in the session backend early and lets parallel Ajax requests continue immediately instead of waiting for the main request to finish. This pattern is particularly effective in themes with many asynchronous frontend widgets.
<?php
declare(strict_types=1);
namespace Mironsoft\Checkout\Controller\Cart;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Session\SessionManagerInterface;
/**
* Example controller releasing the session lock early
* so parallel Ajax requests are not blocked unnecessarily.
*/
class UpdateQuantity extends Action
{
/**
* @param Context $context
* @param SessionManagerInterface $session
*/
public function __construct(
Context $context,
private readonly SessionManagerInterface $session
) {
parent::__construct($context);
}
/**
* Updates cart quantity and releases the session lock immediately.
*
* @return \Magento\Framework\Controller\ResultInterface
*/
public function execute()
{
$this->session->setData('last_cart_action', time());
// No further session writes needed below this point,
// so release the lock before any slower work happens
session_write_close();
// Slow work here no longer blocks parallel Ajax requests
return $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_JSON);
}
}
7. min_lifetime, max_lifetime and session cleanup
min_lifetime and max_lifetime limit the range within which the effective session lifetime may fall, independent of session.cookie_lifetime in the admin configuration. These bounds prevent an accidentally extreme value in the admin UI from flooding the session backend with millions of long lived sessions, which would let memory usage grow uncontrolled.
Unlike the file based session handler, the Redis session backend needs no separate garbage collection cron job, because Redis automatically removes expired keys based on their TTL. That reduces operational effort, but makes correct configuration of first_lifetime, min_lifetime and max_lifetime all the more important, since there is no subsequent cleanup by an external process.
8. Monitoring: analyzing session keys with redis-cli
For diagnosing the session backend, redis-cli -n 2 DBSIZE returns the total number of active sessions in the assigned database. A sudden, unexplained spike usually points to bot traffic without correct user agent detection or to faulty cookie handling, where every request incorrectly creates a new session instead of reusing an existing one.
redis-cli -n 2 --bigkeys finds unusually large session entries, which can result from storing entire product lists or large form data in the session. Such entries should rather be offloaded to a dedicated cache or the database, because they burden the session backend with data that does not need session semantics.
# Total number of active sessions in the session backend database
redis-cli -n 2 DBSIZE
# Find unusually large session entries
redis-cli -n 2 --bigkeys
# Inspect the remaining TTL of a specific session key
redis-cli -n 2 TTL "SESSION_KEY_HERE"
9. Locking strategies compared
The choice of locking strategy directly affects how robustly the session backend handles parallel Ajax requests. The table below compares the common options.
| Strategy | Configuration | Risk | Recommendation |
|---|---|---|---|
| Locking disabled | disable_locking=1 |
Data loss on concurrent writes | Only for provably read only endpoints |
| Default locking | break_after_frontend=5 |
Short wait for stuck requests | Default recommendation for most shops |
| Aggressive break timeout | break_after_frontend=1 |
Race conditions from premature release | Not recommended |
| session_write_close() in code | Explicit in controller | Requires clean code, but safe | Recommended for Ajax heavy actions |
| max_concurrency too low | max_concurrency=1 |
HTTP 503 on parallel Ajax requests | Not recommended for Ajax heavy themes |
| max_concurrency balanced | max_concurrency=6 |
Enough headroom without overload | Keep the Magento default value |
In practice, combining default locking with break_after_frontend=5 and targeted session_write_close() in controllers without further session writes is the most robust solution for the session backend. Fully disabling locking should only happen after carefully verifying that the affected endpoints truly do not create competing writes. max_concurrency should also not be considered in isolation, but always together with break_after_frontend, because both parameters jointly determine how the session backend responds to load spikes.
10. Summary
A performant session backend in Magento does not come from disabling locking, but from precisely controlling it: break_after_frontend and break_after_adminhtml limit wait times in a controlled way, max_concurrency prevents overload during Ajax bursts, and first_lifetime, min_lifetime and max_lifetime control session lifetime without a separate cleanup cron job.
The biggest lever, however, lies in application code: controllers that keep the session open longer than necessary are the most common cause of perceived sluggishness. Explicit session_write_close() after completed writes noticeably relieves the session backend immediately, especially in themes with many parallel Ajax calls.
Redis Session Backend Tuning - The Essentials at a Glance
Do not disable locking
Keep disable_locking=0, instead tune break_after_frontend specifically.
break_after_frontend
Keep the default of 5 seconds, do not artificially shorten it to avoid race conditions.
session_write_close()
Call explicitly in controllers without further writes to avoid Ajax blocking.
Dedicated database index
Separate the session backend from cache and full page cache to avoid key collisions.