Типичная картина: в 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 = 0 | timeout 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 или очередь, а веб-запросы держать в коротких лимитах.