Если браузер одновременно отправляет несколько AJAX-запросов, а PHP отвечает на них один за другим, сначала проверьте блокировку сессии. При стандартном файловом хранении session_start() получает эксклюзивный доступ к данным текущей сессии, поэтому второй запрос с тем же идентификатором ждёт, пока первый завершит работу с $_SESSION.
Решение — не отключать защиту наугад, а измерить ожидание, сократить участок под блокировкой и закрыть сессию до долгой операции. Если запросы меняют одни и те же данные сессии, параллельность нужно проектировать отдельно: раннее снятие блокировки не должно создавать потерянные обновления.
Как выглядит блокировка сессии
Типичная картина заметна только у одного пользователя или в одной вкладке с авторизацией:
- несколько запросов Fetch/XHR стартуют почти одновременно;
- первый обрабатывается долго, а следующие получают растущий TTFB;
- запросы из другого браузера или изолированного тестового профиля не повторяют ту же очередь;
- задержка исчезает, если маршрут не открывает PHP-сессию.
Одна «лесенка» в Network ещё ничего не доказывает. Браузер, прокси, лимит PHP-FPM, медленный запрос к базе и прикладная блокировка тоже могут последовательно задерживать ответы.
Почему параллельные запросы становятся последовательными
Файловый обработчик сессий PHP блокирует файл сессии после session_start(). Другой процесс, который получает тот же session cookie, не может прочитать и изменить эти данные до завершения первого скрипта или вызова session_write_close().
Такая блокировка защищает $_SESSION от гонок. Проблема возникает, когда код держит её во время тяжёлого SQL-запроса, обращения к внешнему API, генерации отчёта, загрузки файла или другого действия, которому сессия уже не нужна.
Важно: очередь формируется для запросов с одной сессией. Если медленно всем посетителям, включая неавторизованных, причина вероятнее находится в пуле обработчиков, базе, внешнем сервисе или общих ресурсах приложения.
Как подтвердить session lock по шагам
1. Воспроизведите один контролируемый сценарий
Откройте DevTools → Network, оставьте Fetch/XHR и запустите два безопасных запроса почти одновременно. Зафиксируйте время старта, TTFB и длительность каждого ответа.
Повторите проверку в изолированном тестовом профиле с другой сессией. Если внутри одной сессии запросы идут очередью, а между двумя сессиями выполняются одновременно, это сильный признак session lock, но ещё не окончательное доказательство.
2. Измерьте ожидание внутри PHP
Поставьте замер непосредственно до и после открытия сессии на тестовом маршруте:
$beforeSession = microtime(true);
session_start();
$afterSession = microtime(true);
error_log(sprintf(
'route=ajax-profile session_open_ms=%.1f',
($afterSession - $beforeSession) * 1000
));
Используйте постоянное безопасное имя маршрута. Не записывайте в лог session ID, Cookie, Authorization, содержимое формы и персональные данные.
Если время на session_start() растёт примерно вместе с длительностью соседнего запроса той же сессии, ожидание найдено. Если задержка возникает раньше запуска PHP или после открытия сессии, продолжайте искать другой ресурс.
3. Найдите запрос, который держит блокировку
Составьте список маршрутов, вызываемых страницей параллельно, и отметьте, где открывается сессия. Начните с самого долгого запроса и проверьте всё, что выполняется между session_start() и закрытием сессии.
Особенно часто блокировка остаётся открытой вокруг:
- запроса к внешнему API;
- долгого SQL или ожидания транзакции;
- генерации файла или отчёта;
- отправки почты;
- сетевого тайм-аута;
- фоновой операции, ошибочно выполняемой в HTTP-запросе.
Сопоставляйте события по безопасному request ID, но не используйте идентификатор сессии как диагностическую метку.
4. Исключите похожие причины
| Наблюдение | Что проверить | |---|---| | Очередь только у одной авторизованной сессии | Время внутри session_start(), момент session_write_close() | | Очередь у разных пользователей | Число свободных PHP-FPM workers, очередь веб-сервера, БД и внешние зависимости | | Ждёт только один тип операции | Транзакцию, mutex, файловую блокировку или лимит конкретного API | | Запрос долго имеет статус Stalled до отправки | Ограничения браузера, соединения, прокси или service worker | | PHP начинает быстро, но поздно отвечает | Код после сессии, SQL, сеть, сериализацию ответа |
HTTP/2 сам по себе не снимает серверную блокировку: несколько потоков могут дойти до разных обработчиков, но всё равно ждать один ресурс сессии.
Как сократить блокировку безопасно
Закройте сессию сразу после чтения и изменений
Скопируйте нужные значения в локальные переменные, сохраните изменения и завершите работу с сессией до долгого участка:
session_start();
$userId = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
$_SESSION['last_action'] = 'report_requested';
session_write_close();
$report = buildReportForUser($userId);
После session_write_close() изменения $_SESSION в этом запросе уже не сохранятся автоматически. Поэтому сначала выполните все обязательные записи, а затем снимайте блокировку.
Используйте режим только для чтения там, где он подходит
Если маршруту достаточно прочитать сессию, можно рассмотреть:
session_start(['read_and_close' => true]);
Проверьте поддержку в вашей версии PHP и поведение выбранного session handler. Для пользовательского обработчика, Redis или другого хранилища правила конкурентного доступа и блокировок могут отличаться от файлового варианта.
Не держите сессию вокруг медленной работы
Проверка прав обычно нужна в начале запроса, но внешний API, построение архива или тяжёлый расчёт не обязаны выполняться под session lock. Передайте им минимальные проверенные значения, а долгую задачу при необходимости вынесите в очередь.
Не отключайте блокировки во всём приложении только ради скорости. Два параллельных запроса могут прочитать старое состояние и перезаписать изменения друг друга. Для счётчиков, корзины, одноразовых действий и смены прав нужна отдельная модель согласованности: транзакция, атомарная операция, версия записи или идемпотентный ключ.
Как проверить исправление
Повторите тот же сценарий с теми же запросами и одной тестовой сессией. Убедитесь, что:
- ожидание внутри
session_start()исчезло или стало коротким; - независимые запросы действительно перекрываются по времени;
- изменения
$_SESSIONсохраняются корректно; - повторный запрос не дублирует действие;
- ошибки и тайм-ауты не оставляют прикладные данные в промежуточном состоянии.
Проверяйте не только скорость, но и результат двух одновременных действий. Иначе ускорение может скрыть гонку.
Если медленны все страницы, начните с проверки времени ответа сайта: session lock объясняет задержку одной сессии, но не общую перегрузку сервера.
Что контролировать после исправления
Публичный мониторинг не воспроизводит Cookie и авторизованный AJAX-сценарий. Web-Puls можно использовать для отдельного контроля доступности и времени ответа публичного URL, а session lock проверять прикладными метриками и тестом одного пользователя.
Если блокировка повторяется, а безопасно разделить чтение и запись сессии не получается, отправьте заявку на профессиональную поддержку, указав публичный адрес, время сбоя, маршрут без секретных параметров и обезличенный замер до и после session_start().