After the upgrade to PHP 8.2, pages failed at random after 30 seconds. The cause was the lock on the session file, not PHP 8

After the move from PHP 7.4 to PHP 8.2, the same page loaded normally one moment and failed after 30 seconds the next. We rebuilt both versions side by side and found the cause: about 20 parallel AJAX calls of one user waited for the lock on the session file, one after another. Fixed with one line per script, without rewriting the application.

  • 20 parallel AJAX calls: 30 s, afterwards 1.5 s
  • Home page waiting: 1.6 s, afterwards 0 s
  • One line per script instead of a new application
At the top under session_start() twenty black bars as a staircase, one after another, up to the 30 s mark; at the bottom under + session_write_close() the same twenty bars in orange side by side, all done at 1.5 s

Web application on Windows Server, Apache 2.4 with mod_php, PHP 7.4.9 to 8.2.35

Industry: Web portals · Duration: 4 hours

Challenge

The home page of the application is rendered in PHP; after it, the browser calls about 20 AJAX scripts in parallel. All scripts include a shared bootstrap file that loads the configuration, the autoloader and the template engine, calls session_start() and writes a value to $_SESSION.

After the PHP upgrade, the client's team reported that the application had become "unevenly slower". Suspects were a generally slower PHP 8 and the sessions. A session_write_close() in one of the AJAX scripts helped, but nobody knew why, or whether PHP 8 had changed how sessions behave. The question: can this be fixed without rewriting the application?

Solution

We rebuilt the application in two Docker containers, php:7.4-apache and php:8.2-apache, Apache with mod_php as at the client, with the same files and the real template engine on every call. A load generator behaved like a browser: one logged-in session, in each round 21 parallel calls with the same cookie, the home page and 20 AJAX calls. A continuous test added six clients sending AJAX calls without pause while the home page was requested every two seconds.

Both versions behaved identically; it was not a regression of PHP 8. The default files handler takes an exclusive, blocking lock on the session file in session_start() and holds it until session_write_close() or the end of the script. All calls of one user therefore run one after another: twenty calls of 1.5 seconds each are 30 seconds for the last one, and a home page clicked meanwhile joins the same queue. On Windows, max_execution_time counts real time, so waiting for the lock alone uses up the limit and ends in a fatal error.

The fix is a switch in the shared bootstrap file: a script that only reads the session releases the lock right after reading it. One line per script, without restructuring the application.

Results

20 parallel AJAX calls of 1.5 s each, time of the last
From 30.0 s to 1.5 s
Home page waiting during the parallel calls
From 1.6 to 1.8 s to 0.00 s
Change to the code
One switch in the bootstrap file, one line per script
Behaviour of the session lock
PHP 7.4 and PHP 8.2 identical

How we worked

  1. Rebuilt the problem under control

    Two containers, PHP 7.4 and PHP 8.2 with Apache and mod_php as at the client, the same files and the real template engine on every call.

  2. Load like a browser

    One logged-in session, 21 parallel calls with the same cookie, the time of each call measured; the work time of a call set by a parameter.

  3. A continuous test from the user's side

    Six clients send AJAX calls without pause, the home page every two seconds; measured is how long the home page waits.

  4. Compared the PHP sources

    The session module, the flock() emulation on Windows, the implementation of the time limit and the UPGRADING notes of all 8.x releases.

  5. Checked side hypotheses

    Opcache, session garbage collection, the behaviour of session_write_close() and a session without a lock.

Measured in both PHP versions

Variant PHP 7.4 PHP 8.2
20 AJAX calls of 1.5 s each, lock not released 30.0 s 30.0 s
The same with session_write_close() right after reading the session 1.5 s 1.5 s
The same with session_start() and the read_and_close option 1.5 s 1.5 s
The same with a session handler without a lock 1.5 s 1.5 s
Continuous test: home page waiting, lock held to the end of the script 1.6 to 1.8 s 1.5 to 1.6 s
Continuous test: home page waiting, lock released early 0.00 s 0.00 s

Challenges we overcame

On Linux the error cannot be reproduced

On Linux, max_execution_time counts the CPU time of the script, and waiting for a lock uses none. On Windows the measured time is real time, as the PHP manual states. On Linux we therefore measured the real duration of the calls and counted anything above 30 seconds as a failure in production.

PHP 8 did not change the lock, but PHP 7.4.22 changed flock() on Windows

The fix for bug #81223 (opens in a new window) of July 2021 locks the whole file on Windows instead of the first byte. The client came from PHP 7.4.9, so for them this was a real difference, just not one of PHP 8. Because locks on Windows are mandatory, since then any other process reading session files, such as a virus scanner or an indexing service, gets in the way. We therefore exclude the session folder from scanning.

Uneven, because the order of waking up is open

Whoever waits for the lock is not served in the order of the requests. Under identical load the home page waited 0 seconds once and 4.5 seconds another time. Hence the impression "sometimes it works, sometimes it does not".

Session garbage collection runs under the lock

It starts at random in session_start(), after the lock is taken, and scans the whole session folder. With 100,000 files this took 0.3 seconds on Linux, much longer on NTFS with a virus scanner. Without php.ini the probability is 1 in 100 per call, not 1 in 1000 as in php.ini-production; with 20 calls per page this hits a noticeable share of page views.

session_write_close() changes the rules without an error

Writing to $_SESSION after closing raises no error and saves nothing. Another session_start() waits for the lock again and replaces $_SESSION with the content of the file, so values set in between are lost. After headers have been sent, the restart fails with a warning. We confirmed each of these behaviours in both versions.

A session without a lock is not a solution

A handler without a lock is possible, but it costs writes: in the test, a slower call overwrote the data that a faster one had saved in parallel with its stale copy. In an application that writes to the session on every call, that means random logouts and vanishing settings. Opcache only speeds things up: a call took 0.5 instead of 1.9 ms, the lock stayed at 30 seconds.

The fix, a switch in the bootstrap file

Instead of adding session_write_close() to every AJAX script separately, the shared bootstrap file got a switch. A script that only reads the session declares this before the include:

define('SESSION_READONLY', true);

The bootstrap file does what it did before and releases the lock right after reading:

$bSessionReadOnly = defined('SESSION_READONLY') && SESSION_READONLY;

session_start();
$_SESSION['marker'] = 0;

if ($bSessionReadOnly) {
    session_write_close();
}

$_SESSION stays readable to the end, the lock lasts a fraction of a millisecond. If such a script rarely does need to write: compute first, then open a short window, write, close, before any output.

Settings without code changes came on top: opcache with file_cache as a fallback on Windows, a dedicated session folder excluded from virus scanning, with a check of the file count and an explicitly set session.gc_divisor. The client received a measuring tool in PHP based on curl_multi that measures a single call and 21 parallel calls on the Windows server before and after the change: if the parallel calls take about 20 times as long as one, they run one after another.

What we take from it

  • "After the upgrade" does not mean regression. Here the upgrade exposed a weakness that had existed for years.
  • Two versions side by side under the same load settle the argument about the cause faster than any discussion.
  • When in doubt, read the sources. A few dozen lines in mod_files.c and flock_compat.c answered "does PHP 8 wait for the session?".
  • Platforms differ. The same code fails on Windows and runs on Linux, only because of how the time limit is defined.
  • The best fix changes the least. One switch where the team looks anyway when sessions cause trouble, instead of a new application or sessions without a lock at the cost of data consistency.

Sources

Related situations

  • Security updates for PHP 8.2 end on 31 December 2026, and for PHP 8.1 and older they have already ended (As of 2026-10). We move long-running PHP applications, including those with Smarty templates and jQuery front ends, to PHP 8.4 or 8.5 and test every stage.

  • Your system runs, but nobody knows it in detail. We record application, database, server and email, assess the risks, recommend the next step in writing and, if you wish, take over operations.

More projects

  • A browser, a box labelled Apache and three lanes to PHP, PHP and CFML; the CFML lane and box are orange

    ColdFusion 2025 in production behind Apache: five traps that are not obvious

    We prepared a production installation of Adobe ColdFusion 2025 on Debian 12, behind an Apache 2.4 that already serves several other sites. The installation itself takes 34 seconds. The attention went into the connector and the configuration details that are easy to miss: an error 503 caused by IPv6, mappings that reach into every virtual host, and a profile that quietly adds a debugger.

  • Four envelopes pass the gates SPF, DKIM and DMARC, each ticked in orange, on the way into the inbox

    Email deliverability for four company domains

    A mail server for four company domains signed nothing, published no DMARC policy and was rejected by Microsoft. Our engineers audited it, piloted the change on one domain and rolled it out in batches. The result: all three checks pass at Gmail, the Microsoft block was lifted, and the users noticed nothing.

In 20 minutes we work out which first step fits your system

You tell us the goal and the current state, and we say openly whether and how we can help. Reply within one working day, no obligation.

Phone (German, Polish, English) +48 501 250 220
Email Email

Reachable: Monday to Friday, 9:00 to 17:00 Central European Time; support and emergencies around the clock (24/7)