Главная » Веб-серверы и PHP » max_execution_time не срабатывает: почему и что делать

max_execution_time не срабатывает: почему и что делать

Типичная картина: в php.ini стоит max_execution_time = 30, а скрипт висит минуту, пять, десять, и PHP-FPM-воркеры заканчиваются один за другим. Ошибки Fatal error: Maximum execution time of 30 seconds exceeded при этом нет. Это не баг, а особенность того, как PHP считает время на Linux.

Коротко: на Linux max_execution_time учитывает процессорное время самого скрипта. Ожидание — sleep(), запросы к базе, сетевые и файловые операции, блокировка файла сессии, внешние программы через system() — в этот лимит не входит. Жёсткий предел по реальному времени задают в PHP-FPM (request_terminate_timeout) и в веб-сервере, а сами ожидания ограничивают таймаутами клиентов БД и HTTP.

Как PHP считает время

В документации к set_time_limit() сказано прямо: функция и директива max_execution_time влияют только на время выполнения самого скрипта; время системных вызовов через system(), потоковых операций, запросов к базе данных и т.п. не учитывается. На Windows, наоборот, считается реальное время.

Технически обычная сборка PHP ставит таймер setitimer(ITIMER_PROF), а он тикает, только пока процесс занимает процессор. Исключение — ZTS-сборки (потокобезопасные, например во FrankenPHP): с PHP 8.3 в них по умолчанию включён --enable-zend-max-execution-timers, и лимит считает прошедшее время, включая ожидание. Пакеты PHP-FPM в Debian, Ubuntu и репозитории Remi для AlmaLinux собраны без ZTS, так что дальше речь о классическом поведении.

Проверить легко:

<?php
set_time_limit(2);
sleep(5);              // ждёт 5 секунд, лимит не срабатывает
echo "Дошли\n";
while (true) {}        // нагружает CPU — через ~2 секунды Fatal error

Когда лимит «не срабатывает»

  • Долгий запрос к MySQL/MariaDB. PHP ждёт ответа в read() на сокете, процессор не занят.
  • HTTP-запросы через cURL или file_get_contents() к медленному API.
  • Файловые операции на медленном или зависшем хранилище (NFS, перегруженный диск): процесс ждёт ввода-вывода.
  • Блокировка файла сессии. Стандартный обработчик сессий files держит эксклюзивную блокировку (flock) на файл сессии до конца запроса. Второй запрос того же пользователя ждёт её освобождения — и это ожидание тоже не считается.
  • Внешние программы через exec(), system(), shell_exec() работают отдельными процессами.
  • Вызов set_time_limit() в цикле. Каждый вызов обнуляет счётчик, поэтому импорт с set_time_limit(30) на каждой итерации не остановится никогда.
  • CLI. В командной строке значение по умолчанию 0 — без ограничения.

Блокировка сессии: как её снять

Если скрипт после session_start() больше не пишет в сессию, закройте её сразу:

session_start();
$userId = $_SESSION['user_id'] ?? null;
session_write_close();   // блокировка снята, параллельные запросы не ждут
// ... долгая работа ...

Для чтения без блокировки есть вариант session_start(['read_and_close' => true]);.

Жёсткий лимит: request_terminate_timeout в PHP-FPM

Именно для этого случая в документации PHP-FPM описан request_terminate_timeout: таймаут на обработку одного запроса, после которого воркер убивается; его рекомендуют, когда max_execution_time по какой-то причине не останавливает скрипт. По умолчанию 0 (выключено), единицы — s, m, h, d.

Файл пула:

  • Debian/Ubuntu — /etc/php/8.x/fpm/pool.d/www.conf (служба php8.x-fpm);
  • AlmaLinux/Rocky — /etc/php-fpm.d/www.conf (служба php-fpm).
; www.conf
request_terminate_timeout = 120s
request_slowlog_timeout = 20s
slowlog = /var/log/php-fpm/www-slow.log

request_slowlog_timeout пишет бэктрейс медленного запроса в slowlog — по нему сразу видно, на каком вызове скрипт ждал. Путь к slowlog укажите существующий. Параметр request_terminate_timeout_track_finished = yes (с PHP 7.3) распространяет лимит и на код после fastcgi_finish_request(). После правки проверьте конфиг и перечитайте его (версию подставьте свою):

php-fpm8.3 -t && systemctl reload php8.3-fpm    # Debian/Ubuntu
php-fpm -t && systemctl reload php-fpm          # AlmaLinux

Учтите: убитый воркер не выполнит register_shutdown_function() и не допишет начатое. Ставьте этот лимит заметно больше max_execution_time. Если на сервере несколько версий PHP, таймаут задаётся в пуле каждой — подробнее в статье несколько версий PHP-FPM на одном сервере.

Таймауты веб-сервера

Веб-сервер может оборвать ожидание раньше или позже PHP:

  • nginx — fastcgi_read_timeout (по умолчанию 60s). При превышении клиент получает 504 Gateway Time-out, в error.log — upstream timed out (110: Connection timed out) while reading response header from upstream. Скрипт в PHP-FPM при этом продолжает работать, пока его не остановит request_terminate_timeout.
  • Apache — Timeout (по умолчанию 60 секунд) и ProxyTimeout для mod_proxy_fcgi (по умолчанию равен Timeout).
# nginx, внутри location ~ \.php$
fastcgi_read_timeout 120s;

# Apache
ProxyTimeout 120

Я выставляю цепочку так: max_execution_time < request_terminate_timeout ≤ таймаут веб-сервера. Как собрать nginx и PHP-FPM целиком — в заметке про настройку VPS для WordPress.

Ограничьте сами ожидания

  • MySQL 8.0/8.4: системная переменная max_execution_time (в миллисекундах, действует на SELECT) или подсказка SELECT /*+ MAX_EXECUTION_TIME(5000) */ …. MariaDB: max_statement_time в секундах.
  • mysqli: $mysqli->options(MYSQLI_OPT_READ_TIMEOUT, 10);
  • cURL: CURLOPT_CONNECTTIMEOUT и CURLOPT_TIMEOUT; потоки PHP — default_socket_timeout.

Медленные запросы лучше лечить в самой базе — см. оптимизацию MySQL 8 и MariaDB.

Как проверить действующее значение

У CLI и PHP-FPM разные php.ini, поэтому php -i в консоли покажет не то, что видит сайт:

php -r 'var_dump(ini_get("max_execution_time"));'   # CLI: обычно "0"
php --ini                                            # какие ini читает CLI

Для веба положите временный файл и откройте его в браузере (потом удалите):

<?php
var_dump(ini_get('max_execution_time'), php_ini_loaded_file());

Если значение задано в пуле через php_admin_value[max_execution_time], переопределить его из скрипта через ini_set() нельзя.

СимптомПричинаРешение
Скрипт висит дольше лимита, без Fatal errorОжидание БД, сети, дискаrequest_terminate_timeout, таймауты клиента БД и cURL
Параллельные запросы одного пользователя идут по очередиБлокировка файла сессииsession_write_close() после чтения сессии
Браузер получает 504, а процесс PHP живСработал fastcgi_read_timeout раньше PHPСогласовать таймауты nginx и PHP-FPM
Консольный скрипт работает бесконечноВ CLI max_execution_time = 0timeout 600 php script.php или лимит в коде

Частые вопросы

Почему sleep(60) не прерывается при max_execution_time = 30?

Во время sleep() процесс не использует процессор, а на Linux лимит считает именно процессорное время. На Windows и в ZTS-сборках с execution timers тот же скрипт будет остановлен.

Что такое max_input_time и связан ли он с этим?

Это лимит на разбор входных данных (POST, GET, загрузка). По умолчанию -1 — используется значение max_execution_time. На зависания во время работы скрипта он не влияет.

Можно ли выставить max_execution_time = 0 для сайта?

Технически да, но тогда один зациклившийся скрипт может занять воркер навсегда. Для долгих задач лучше вынести их в CLI или очередь, а веб-запросы держать в коротких лимитах.