Ошибка Maximum execution time exceeded означает, что PHP-скрипт выполнялся дольше разрешенного времени и был остановлен. Часто она появляется при импорте, формировании отчета, обработке изображений, резервном копировании, обращении к внешнему API или выполнении тяжелого запроса к базе.

Увеличение max_execution_time иногда дает время завершить разовую операцию, но не устраняет бесконечный цикл, медленный SQL или зависший сетевой запрос. Сначала нужно определить, где расходуется время и какой слой фактически завершает запрос: PHP, PHP-FPM, Nginx, Apache, proxy или балансировщик.

Как выглядит ошибка

  • В журнале PHP есть Fatal error с текстом Maximum execution time exceeded.
  • Административная операция долго загружается и заканчивается пустой страницей или ошибкой 500.
  • Импорт каждый раз останавливается примерно на одной и той же строке.
  • Отчет работает на малом периоде, но падает на большом.
  • Через браузер задача завершается ошибкой, а из командной строки успевает выполниться.
  • Вместо PHP-ошибки клиент получает 504 Gateway Timeout.
  • После увеличения лимита процесс начинает занимать память или блокировать другие запросы.

Сначала найдите точный запрос и время

  1. Запишите URL, действие пользователя и параметры проблемной операции.
  2. Зафиксируйте время начала и завершения с точностью до секунд.
  3. Найдите соответствующие записи в журналах PHP-FPM, веб-сервера и приложения.
  4. Определите файл и строку из Fatal error, но не считайте их автоматически первопричиной.
  5. Повторите операцию на безопасной копии с тем же объемом данных.
  6. Не включайте публичный display_errors на рабочем сайте.

Почему строка из ошибки может вводить в заблуждение

PHP указывает место, где закончился лимит, а не обязательно место, которое потратило основное время. Скрипт мог двадцать секунд ждать внешний API, затем выполнить обычную строку, на которой истекла последняя секунда. Поэтому нужен профиль всей операции или хотя бы замеры длительности этапов.

  • Логируйте начало и завершение крупных этапов с correlation id.
  • Измеряйте длительность SQL, HTTP-запросов, чтения файлов и обработки каждой партии.
  • Не записывайте в журнал пароли, токены и полное содержимое пользовательских данных.
  • Сравните время PHP с длительностью запроса по журналу веб-сервера.

Проверьте фактический лимит PHP

Настройки командной строки, веб-сервера и разных пулов PHP-FPM могут отличаться. Изменение одного php.ini не гарантирует, что сайт использует новое значение.

  • Определите версию PHP и пул, обслуживающий конкретный домен.
  • Проверьте effective max_execution_time внутри веб-контекста без публикации полного phpinfo.
  • Учитывайте pool overrides, php_admin_value, настройки хостинга и параметры приложения.
  • После изменения перечитайте конфигурацию нужного сервиса и проверьте журнал ошибок.
  • Помните, что CLI-задачи могут иметь другой лимит или не иметь его вовсе.

Другие таймауты в цепочке запроса

Даже если PHP разрешено работать дольше, соединение может раньше закрыть PHP-FPM, Nginx, Apache, reverse proxy, CDN или браузер. Значения должны соответствовать назначению операции, но делать каждый HTTP-запрос бесконечным не следует.

  • PHP-FPM request_terminate_timeout может принудительно завершать воркер.
  • Nginx fastcgi_read_timeout ограничивает ожидание ответа FastCGI.
  • Proxy и балансировщик имеют собственный upstream timeout.
  • Внешний API должен вызываться с явными connect и response timeout.
  • Клиент может прекратить ожидание, хотя серверный процесс продолжит работу.

Проверьте бесконечные и слишком большие циклы

  • Условие выхода никогда не меняется или зависит от неправильного значения.
  • Пагинация API каждый раз возвращает и обрабатывает одну страницу.
  • Рекурсивный обход повторно посещает уже обработанные узлы.
  • Вложенные циклы сравнивают каждую запись с каждой и растут квадратично.
  • Повтор запроса не имеет максимального числа попыток и backoff.
  • Обработка всех записей выполняется одним HTTP-запросом без партий.

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

Медленные запросы к базе

Один SQL-запрос может занять большую часть времени. Особенно часто проблема появляется при фильтрации без индекса, сортировке большого набора, N+1-запросах и блокировках транзакций.

  • Включите безопасный slow query log или профилирование на ограниченный период.
  • Проверьте план выполнения проблемного запроса.
  • Убедитесь, что индексы соответствуют фильтрации, соединению и сортировке.
  • Не загружайте все строки в PHP, если агрегацию может выполнить база.
  • Ищите блокировки и длительные транзакции, а не только медленный CPU.
  • Проверьте N+1: запрос внутри цикла часто незаметен на малом наборе.

Внешний API, DNS и сеть

Интеграция может ждать ответ стороннего сервиса до общего лимита PHP. Каждый сетевой вызов должен иметь отдельные таймауты и контролируемое число повторов.

  • Разделите connect timeout и ожидание ответа.
  • Не выполняйте бесконечные retry внутри одного запроса пользователя.
  • Проверяйте HTTP-код, размер ответа и валидность следующей страницы.
  • Выносите повторную обработку во фоновую очередь.
  • Используйте идемпотентность, чтобы retry не создавал дубль операции.
  • Мониторьте задержку провайдера отдельно от времени собственного кода.

Файлы, изображения и архивы

  • Большое изображение распаковывается в памяти и обрабатывается слишком долго.
  • Скрипт рекурсивно обходит каталог с сотнями тысяч файлов.
  • Архив создается на медленном диске или в каталоге без свободного места.
  • Сетевое хранилище отвечает с большой задержкой.
  • Антивирус или резервное копирование одновременно блокирует файлы.
  • Операция каждый раз заново обрабатывает уже готовые объекты.

Импорт и экспорт нужно делить на партии

Большой импорт не должен зависеть от одного открытого HTTP-соединения. Разделите данные на ограниченные партии, сохраняйте checkpoint и показывайте пользователю состояние задачи.

  1. Проверьте и сохраните исходный файл.
  2. Создайте задачу с уникальным идентификатором.
  3. Обрабатывайте ограниченное число строк в одной транзакции.
  4. Фиксируйте прогресс и ошибки отдельных строк.
  5. Продолжайте следующую партию фоновым воркером.
  6. После завершения выполните контроль количества и итоговых сумм.

Когда допустимо временно увеличить лимит

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

  • Сначала убедитесь, что нет бесконечного цикла и блокировки.
  • Оцените память, CPU, соединения с базой и длительность транзакции.
  • Не ставьте нулевой лимит глобально для всех веб-запросов.
  • Зафиксируйте временное изменение и верните обычное значение после операции.
  • Не увеличивайте только PHP timeout, забывая о FPM и proxy.

Как исправить причину

  • Бесконечный цикл: исправить условие выхода и добавить защитный предел.
  • N+1 и медленный SQL: изменить запрос, добавить подходящий индекс и измерить план.
  • Внешний API: настроить короткие таймауты, очередь, retry с backoff и идемпотентность.
  • Большой импорт: разбить на партии и сохранять прогресс.
  • Тяжелый отчет: предварительно агрегировать данные или формировать файл фоново.
  • Обработка изображений: ограничить размеры, выполнять асинхронно и переиспользовать результат.
  • Блокировка: сократить транзакцию и устранить конкурирующий длительный процесс.

Пошаговая проверка результата

  1. Повторите операцию на том же контрольном объеме данных.
  2. Сравните длительность каждого этапа до и после исправления.
  3. Убедитесь, что результат полный и не содержит дублей.
  4. Проверьте малый, средний и предельный допустимый объем.
  5. Запустите несколько параллельных обычных запросов и оцените влияние на сайт.
  6. Проверьте журнал PHP-FPM, веб-сервера, базы и очереди.
  7. Убедитесь, что временно повышенный лимит возвращен к штатному значению.

Типичные неправильные действия

  • Поставить max_execution_time в 0 глобально и считать проблему решенной.
  • Повысить таймаут до часа для обычного пользовательского запроса.
  • Ориентироваться только на строку Fatal error и не измерять предыдущие этапы.
  • Менять CLI php.ini, хотя сайт использует другой PHP-FPM pool.
  • Увеличить Nginx timeout, оставив процесс PHP ограниченным прежним значением.
  • Повторять зависший API-запрос без лимита и паузы.
  • Запускать большой импорт заново после каждой ошибки без checkpoint.

Как предотвратить повторение

  • Добавьте метрики длительности для SQL, внешних API и фоновых задач.
  • Ограничивайте размер пользовательского запроса и объем одной партии.
  • Переносите длительные операции из HTTP в очередь.
  • Настройте slow query log и алерты на рост времени ответа.
  • Тестируйте код на реалистичном объеме данных, а не на десяти строках.
  • Устанавливайте явные сетевые timeout и предел повторов.
  • Документируйте лимиты PHP-FPM, веб-сервера и proxy для разных типов задач.

Итог

Ошибка Maximum execution time exceeded показывает, что операция не уложилась в лимит PHP, но сама по себе не раскрывает причину. Нужно измерить этапы, проверить SQL, внешние сервисы, циклы, файлы и всю цепочку таймаутов. Для тяжелых задач надежнее партии и фоновые воркеры, а не бесконечный веб-запрос.

Если ошибка повторяется, я могу найти медленный участок по журналам и замерам, оптимизировать запросы или код, вынести длительную операцию в очередь и согласовать безопасные лимиты PHP-FPM и веб-сервера.