{"record":{"id":"74c0603f777cbf0d","repo":"symfony/http-foundation","slug":"failed-to-read-session-insert-reported-a-duplicate-id-but","errorCode":null,"errorMessage":"Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.","messagePattern":"Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"Session/Storage/Handler/PdoSessionHandler.php","lineNumber":718,"sourceCode":"        while (true) {\n            $selectStmt->execute();\n            $sessionRows = $selectStmt->fetchAll(\\PDO::FETCH_NUM);\n\n            if ($sessionRows) {\n                $expiry = (int) $sessionRows[0][1];\n\n                if ($expiry < time()) {\n                    $this->sessionExpired = true;\n\n                    return '';\n                }\n\n                return \\is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];\n            }\n\n            if (null !== $insertStmt) {\n                $this->rollback();\n                throw new \\RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.');\n            }\n\n            if (!filter_var(\\ini_get('session.use_strict_mode'), \\FILTER_VALIDATE_BOOL) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) {\n                // In strict mode, session fixation is not possible: new sessions always start with a unique\n                // random id, so that concurrency is not possible and this code path can be skipped.\n                // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block\n                // until other connections to the session are committed.\n                try {\n                    $insertStmt = $this->getInsertStatement($sessionId, '', 0);\n                    $insertStmt->execute();\n                } catch (\\PDOException $e) {\n                    // Catch duplicate key error because other connection created the session already.\n                    // It would only not be the case when the other connection destroyed the session.\n                    if (str_starts_with($e->getCode(), '23')) {\n                        // Retrieve finished session data written by concurrent connection by restarting the loop.\n                        // We have to start a new transaction as a failed query will mark the current transaction as\n                        // aborted in PostgreSQL and disallow further queries within it.\n                        $this->rollback();","sourceCodeStart":700,"sourceCodeEnd":736,"githubUrl":"https://github.com/symfony/http-foundation/blob/5aea19cd678fa4140f6108406f1096de5e9ed6e4/Session/Storage/Handler/PdoSessionHandler.php#L700-L736","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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()."],"exampleFix":"// before (php.ini)\nsession.use_strict_mode = 0\n\n// after (bootstrap, before session_start)\nini_set('session.use_strict_mode', '1');","handlingStrategy":"try-catch","validationCode":"// before session_start\nif (!filter_var(ini_get('session.use_strict_mode'), FILTER_VALIDATE_BOOL)) {\n    trigger_error('Enable session.use_strict_mode for PDO session handler transactional locking', E_USER_WARNING);\n}","typeGuard":null,"tryCatchPattern":"try {\n    $session->start();\n} catch (\\RuntimeException $e) {\n    if (str_contains($e->getMessage(), 'INSERT reported a duplicate id')) {\n        $session->invalidate(); // drop raced session, start fresh\n        $session->start();\n    } else {\n        throw $e;\n    }\n}","preventionTips":["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"],"tags":["sessions","pdo","concurrency","race-condition","php"],"backgroundTag":"internal-invariant-violation","analyzedSha":"5aea19cd678fa4140f6108406f1096de5e9ed6e4","analyzedAt":"2026-09-13T01:52:22.855Z","contentChangedAt":"2026-09-13T01:52:22.855Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}