symfony/http-foundation · error · RuntimeException
Failed to read session: INSERT reported a duplicate id but…
Error message
Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.
What it means
This RuntimeException is thrown by PdoSessionHandler::doRead() as an internal invariant check during transactional session locking. When a session row does not exist, the handler optimistically INSERTs a placeholder row to block concurrent writers; if that INSERT fails with a SQLSTATE 23x duplicate-key error (meaning another connection just created the row), the handler retries the SELECT expecting to find that row. If the SELECT still returns nothing, the handler's assumptions are broken — the row was deleted between the duplicate-key error and the re-read — so it rolls back and throws rather than returning corrupted or empty data.
Solutions
- Enable session.use_strict_mode=1 in php.ini (ini_set before session_start) so this non-strict concurrency path in doRead() is skipped entirely and new ids are always unique random values.
- Eliminate concurrent destructive operations on the same session id: avoid calling session_destroy()/session_regenerate_id(true) racing with parallel requests, or serialize requests per session (e.g. default session locking files before switching to PDO, or application-level locking).
- Stop external jobs from deleting rows of live sessions: exclude rows whose session_time is within session.gc_maxlifetime, or pause GC while requests run.
- Catch the \RuntimeException around session_start()/session read and recover by regenerating a fresh session id and restarting the request flow.
- Ensure the table actually has the primary/unique key on the id column as the schema requires (the duplicate-key path depends on it), using the CREATE TABLE from the handler docs or createTable().
Example fix
// before (php.ini)
session.use_strict_mode = 0
// after (bootstrap, before session_start)
ini_set('session.use_strict_mode', '1'); Defensive patterns
Strategy: try-catch
Validate before calling
// before session_start
if (!filter_var(ini_get('session.use_strict_mode'), FILTER_VALIDATE_BOOL)) {
trigger_error('Enable session.use_strict_mode for PDO session handler transactional locking', E_USER_WARNING);
} Try / catch
try {
$session->start();
} catch (\RuntimeException $e) {
if (str_contains($e->getMessage(), 'INSERT reported a duplicate id')) {
$session->invalidate(); // drop raced session, start fresh
$session->start();
} else {
throw $e;
}
} Prevention
- Always set session.use_strict_mode=1 with PdoSessionHandler transactional locking
- Avoid concurrent session_destroy()/session_regenerate_id(true) racing parallel requests
- Do not run external GC/DELETE jobs on the session table for live sessions
- Keep the primary key on the session id column exactly as the handler schema specifies
When it happens
Trigger: Using PdoSessionHandler with lock_mode LOCK_TRANSACTIONAL while session.use_strict_mode=0 (non-strict mode), when: (1) a concurrent request that had the same session id destroys the session (e.g. session_destroy() / doDestroy) between the failed INSERT and the retry SELECT; (2) heavy concurrency races on the same session id where one connection deletes the row while another is re-reading after its duplicate-key error; (3) external garbage collection or manual DELETE removes the row in that window.
Common situations: High-concurrency PHP-FPM setups with many parallel AJAX requests sharing one session id; apps that call session_regenerate_id(true) or session_destroy() concurrently with in-flight requests; load tests hammering the same session; mixing the PDO session table with external cleanup cron jobs that delete session rows while requests are in flight.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- The session handler " " does not support clearing all…
- Invalid argument $savePath
- " " requires PDO error mode attribute be set to throw…
- Creating the session table is currently not implemented for…
- SQLite does not support advisory locks.
AI-assisted analysis of symfony/http-foundation@5aea19cd67 (2026-09-13).
Data as JSON: /api/errors/74c0603f777cbf0d.
Report an issue: GitHub.
Appendix: source
Thrown at Session/Storage/Handler/PdoSessionHandler.php:718
while (true) {
$selectStmt->execute();
$sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM);
if ($sessionRows) {
$expiry = (int) $sessionRows[0][1];
if ($expiry < time()) {
$this->sessionExpired = true;
return '';
}
return \is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
}
if (null !== $insertStmt) {
$this->rollback();
throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.');
}
if (!filter_var(\ini_get('session.use_strict_mode'), \FILTER_VALIDATE_BOOL) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) {
// In strict mode, session fixation is not possible: new sessions always start with a unique
// random id, so that concurrency is not possible and this code path can be skipped.
// Exclusive-reading of non-existent rows does not block, so we need to do an insert to block
// until other connections to the session are committed.
try {
$insertStmt = $this->getInsertStatement($sessionId, '', 0);
$insertStmt->execute();
} catch (\PDOException $e) {
// Catch duplicate key error because other connection created the session already.
// It would only not be the case when the other connection destroyed the session.
if (str_starts_with($e->getCode(), '23')) {
// Retrieve finished session data written by concurrent connection by restarting the loop.
// We have to start a new transaction as a failed query will mark the current transaction as
// aborted in PostgreSQL and disallow further queries within it.
$this->rollback();View on GitHub (pinned to 5aea19cd67)