skip to content

A PHP shopping-basket page fires four AJAX requests at once, yet they finish one after another instead of in parallel; why, and how do you fix it?

level: seniorimportance: should knowfreq 50%

answer

  1. same browser, same session ID
  2. files handler takes an exclusive flock
  3. lock held until write or script end
  4. session_write_close() early
  5. read_and_close for read-only endpoints

basics

~20 s

The default files handler holds an exclusive lock on the session file from session_start() until the session is written, so requests sharing one session ID queue. Release it early with session_write_close(), or use read_and_close where nothing is written.

solid answer

~40 s

All four requests carry the same `PHPSESSID`, and the default `files` save handler takes an exclusive `flock()` on `sess_<id>` inside `session_start()`. The lock is held until `session_write_close()` (or `session_commit()`) runs, or until the script ends, so the second request blocks in `session_start()` until the first finishes, and so on. Requests from other visitors are unaffected. The fix is to hold the lock only while touching `$_SESSION`: endpoints that only read use `session_start(['read_and_close' => true])`, which loads the data and releases the lock immediately; endpoints that write change `$_SESSION`, call `session_write_close()`, and only then do the slow work. Removing locking altogether is not a fix: two basket updates would then overwrite each other.

code

php · 8 lines
php
<?php
declare(strict_types=1);

// basket-count.php: read-only endpoint
session_start(['read_and_close' => true]);   // loads $_SESSION, releases the lock at once

header('Content-Type: application/json');
echo json_encode(['items' => count($_SESSION['basket'] ?? [])]);

go deeper

for a junior

Recall that requests from one browser share a session ID and that session_write_close() ends the session for the rest of the script.

for a middle

Explain the files handler's exclusive lock, when it is taken and released, and what read_and_close does differently from a plain session_start().

for a senior

Diagnose the staircase from timings, restructure endpoints into readers and early-closing writers, and explain why removing the lock trades waiting for lost updates.

for a principal

Set a team rule for session use in AJAX-heavy pages: small sessions, read_and_close by default, and a reviewed decision on locking whenever the store changes.

## Why the requests queue A browser sends the same session cookie with every request, so four AJAX calls from one page all carry **the same session ID**. PHP's default `files` save handler protects each session with an **exclusive file lock**: 1. `session_start()` opens `sess_<id>` and calls `flock()` with `LOCK_EX`. 2. The lock stays held while the script runs, because PHP must write `$_SESSION` back at the end. 3. It is released when the session is closed: usually by `session_write_close()` (alias `session_commit()`), also by `session_abort()` or `session_destroy()`, or automatically at the end of the script. The second request therefore waits inside `session_start()` until the first releases the lock, the third waits for the second, and so on. In the browser's network panel the four requests start together but end in a staircase, and each one's server time includes the time spent waiting. Requests from **other** visitors use other session files and do not wait. ## Why the lock exists Locking keeps session data consistent. Without it, two requests could both read a basket of two items, each add one, and each write back three items: the last write wins and one addition disappears. The PHP manual calls locking mandatory for consistency, and also notes the flip side: a slow request holding the lock can be abused to stall a victim's session, so the advice is to minimise how long locks are held. ## Fix 1: read-only endpoints use read_and_close An endpoint that only *reads* the basket (a counter badge, a mini-cart) does not need the lock after loading: - `session_start(['read_and_close' => true])` reads `$_SESSION` and closes the session straight away, releasing the lock. - `$_SESSION` stays populated for the rest of the script. - Nothing written to it afterwards is saved. ## Fix 2: writers close early An endpoint that adds an item must write, but it rarely needs the lock for its whole duration: - start the session, update `$_SESSION`, copy what you still need into local variables; - call `session_write_close()` to write and release the lock; - then do the slow part: price lookups, stock checks, remote API calls, rendering. After the close, changes to `$_SESSION` are **not** saved and PHP gives no warning; the manual states the module does not detect modification of an inactive session. If a script genuinely needs to write again later, it can call `session_start()` again, but only while headers are still unsent. ## Other ways a session closes Besides `session_write_close()`, two calls end the session and release the lock early: - `session_abort()` closes the session **without** saving: any changes made to `$_SESSION` in this request are discarded. Useful when a request detects it should not persist anything. - `session_destroy()` deletes the stored record for the current ID and ends the session; it is a sign-out tool, not a performance tool. `session_reset()`, by contrast, reloads `$_SESSION` from storage for a session that stays active, so it keeps the lock. ## What does not fix it | Idea | Why it fails | |---|---| | More PHP workers | the requests are not waiting for a worker, they are waiting for one file lock | | `session.lazy_write=1` | it skips rewriting unchanged data but does not shorten the lock | | A custom handler without locking | removes the wait but brings back lost updates | | Longer timeouts | hides the queue instead of removing it | ## Custom handlers and locking The lock is a property of the `files` handler, not of the session API. A handler you write with `SessionHandlerInterface` gets no locking unless it implements its own; other built-in or extension handlers vary. So when an application moves its sessions to a different store, the staircase can disappear while silent lost updates appear, which is the harder bug to spot. Whichever store you use, keeping the session small and closing it early reduces both problems. ## A quick diagnostic checklist - Do the stalled requests share one session ID? If not, look elsewhere. - Is the wait inside `session_start()`? A profile or a timing log around it confirms the lock. - Which endpoints write to `$_SESSION` at all? Many can switch to `read_and_close`. - Is slow I/O happening while the session is still open? Move it after `session_write_close()`.

  • What happens to a value assigned to $_SESSION after session_write_close()?
    It exists for the rest of the current request only. The session is inactive, PHP does not detect the change, and nothing writes it to storage, so the next request never sees it. No warning is raised. Make every change before closing, or call `session_start()` again while headers are still unsent.
  • Does switching to a custom SessionHandlerInterface store remove the queueing safely?
    It removes the queueing only because such a handler has no lock unless you write one. Concurrent requests for one session then read the same data and the last write wins, so a basket addition can vanish. Either implement locking in the handler or keep writes rare and short.
  • Would adding more PHP worker processes make the four requests run in parallel?
    No. The requests are already running in separate workers; they are blocked on the same session file lock inside `session_start()`. Extra workers just wait on it too. Only releasing the lock earlier, or not taking it for read-only endpoints, restores parallelism.

The session file is a shared shopping list on one clipboard: whoever holds it makes everyone else wait, even people who only want to read it. read_and_close is glancing at the list and handing the clipboard straight back.

saying these in an interview costs you the question

  • PHP can only serve one request per visitor at a time
  • The session lock blocks requests from every visitor on the server
  • session_write_close() destroys the session data
  • Changes to $_SESSION after session_write_close() are saved at script end
  • A lock-free custom handler is a free performance win
  • read_and_close still holds the lock until the script ends