Большие файлы в PHP: чек-лист перед продакшеном
Загрузка файла на 2 ГБ падает с ошибкой, скрипт съедает всю память, а Nginx отдаёт 413 ещё до того, как PHP успел вмешаться. Разбираем, что проверить до релиза.

Пользователь загружает архив на 800 МБ, а в логах — Allowed memory size of 134217728 bytes exhausted. Или загрузка обрывается на трёх минутах без единой записи в лог. Или файл в 5 ГБ читается через file_get_contents и кладёт весь сервер. Большие файлы в PHP ломаются не в одном месте, а в цепочке: веб-сервер, лимиты PHP, способ чтения и запись на диск. Проверять надо всю цепочку — иначе поднимете один лимит и упрётесь в следующий.
Ниже — чек-лист по каждому звену. Актуально для PHP 8.1–8.3 с Nginx или Apache. Где поведение зависит от версии — отмечено отдельно.
Где именно рвётся цепочка
Запрос с большим файлом проходит через несколько барьеров, и каждый может отклонить его первым:
- Веб-сервер (Nginx/Apache) — режет тело запроса по своему лимиту ещё до PHP.
- PHP-лимиты приёма —
upload_max_filesizeиpost_max_size. - Память процесса —
memory_limit, если файл читается целиком. - Время выполнения —
max_execution_timeплюс таймауты FastCGI. - Диск — временная директория и место под финальный файл.
Поднять один лимит и забыть про остальные — классическая ошибка. Разберём по звеньям.
Барьер 1: веб-сервер режет первым
Nginx по умолчанию отдаёт 413 Request Entity Too Large, если тело запроса больше client_max_body_size (по умолчанию 1 МБ). Причём отдаёт до того, как PHP вообще увидит запрос, — так что никакие настройки PHP тут не помогут.
# /etc/nginx/nginx.conf или в нужном server/location
client_max_body_size 2048M;
# таймаут на чтение тела от клиента — медленная загрузка большого файла
client_body_timeout 300s;
# таймауты между Nginx и PHP-FPM
fastcgi_read_timeout 300s;
fastcgi_send_timeout 300s;После правки — nginx -t и systemctl reload nginx. Если nginx -t ругается на синтаксис, значение client_max_body_size вы вписали не в тот контекст: оно допустимо в http, server и location.
Для Apache через mod_php отдельного аналога client_max_body_size нет — там всё упирается в лимиты PHP ниже. Но если стоит mod_security, у него свой лимит тела запроса (SecRequestBodyLimit), и он тоже вернёт 413.
Барьер 2: лимиты приёма PHP
Два параметра в php.ini работают в паре, и здесь новички ошибаются чаще всего:
| Параметр | Что ограничивает | Правило |
|---|---|---|
upload_max_filesize | Размер одного загружаемого файла | Не больше post_max_size |
post_max_size | Размер всего POST-тела | Больше upload_max_filesize, с запасом на другие поля |
Если post_max_size меньше upload_max_filesize, файл просто не дойдёт: PHP обрежет POST раньше. При превышении post_max_size массив $_FILES приходит пустым, а не с ошибкой — это сбивает с толку. Проверять надо так:
<?php
// PHP 8.x
$contentLength = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
$postMax = ini_get('post_max_size');
// $_POST и $_FILES пусты, но тело пришло — значит превышен post_max_size
if ($contentLength > 0 && empty($_POST) && empty($_FILES)) {
http_response_code(413);
exit('Файл превышает лимит post_max_size: ' . $postMax);
}Отдельно проверяйте $_FILES['field']['error']. Код UPLOAD_ERR_INI_SIZE означает превышение upload_max_filesize, UPLOAD_ERR_FORM_SIZE — превышение MAX_FILE_SIZE из формы, UPLOAD_ERR_PARTIAL — файл дошёл частично (обрыв связи).
Барьер 3: память — не читайте файл целиком
Главная причина падений с memory_exhausted — попытка загрузить содержимое в переменную:
<?php
// ТАК НЕ НАДО: файл на 2 ГБ = 2 ГБ в памяти процесса
$data = file_get_contents('/tmp/bigfile.zip');
$hash = md5($data);Правильно — читать потоком, фиксированными блоками. Память при этом остаётся на уровне размера буфера, независимо от размера файла:
<?php
// Потоковое чтение блоками по 8 КБ — память не растёт с размером файла
$handle = fopen('/tmp/bigfile.zip', 'rb');
if ($handle === false) {
throw new RuntimeException('Не удалось открыть файл');
}
$hashContext = hash_init('sha256');
while (!feof($handle)) {
$chunk = fread($handle, 8192); // 8 КБ за итерацию
hash_update($hashContext, $chunk);
}
fclose($handle);
$hash = hash_final($hashContext);Для копирования между потоками есть stream_copy_to_stream — она сама читает блоками и не держит файл в памяти. Для построчной обработки текстовых логов — fgets в цикле по feof, а ещё лучше генератор с yield, чтобы не собирать строки в один массив.
Правило простое: если размер файла заранее неизвестен, любая функция, которая возвращает всё содержимое разом —file_get_contents,file,readfileв переменную — потенциальная бомба. Считайте, что файл всегда больше вашей памяти.
Отдельный случай — отдача большого файла на скачивание. readfile() без вывода в переменную буферизует данные, но при включённом output_buffering может держать их в памяти. Перед отдачей вызовите while (ob_get_level()) { ob_end_clean(); }, а лучше отдавайте статику через X-Accel-Redirect (Nginx) или X-Sendfile (Apache) — тогда файл вообще не проходит через PHP.
Барьер 4: время и таймауты
Заливка гигабайта по медленному каналу занимает минуты. max_execution_time в PHP считает только процессорное время скрипта и не учитывает время ожидания загрузки тела запроса — так что сам он редко виноват. А вот fastcgi_read_timeout в Nginx и request_terminate_timeout в PHP-FPM оборвут долгий запрос молча, вернув 504.
max_execution_time— на время обработки после приёма файла (распаковка, конвертация).request_terminate_timeoutв пуле PHP-FPM — жёсткий предел жизни процесса, перекрываетmax_execution_time.fastcgi_read_timeout(Nginx) — сколько Nginx ждёт ответа от FPM.
Все три должны быть согласованы. Если fastcgi_read_timeout = 60s, а обработка идёт 90s — получите 504 при живом PHP-процессе.
Барьер 5: диск и временные файлы
PHP кладёт загруженный файл во временную директорию (upload_tmp_dir, по умолчанию системная /tmp) до вызова move_uploaded_file. Три проблемы, которые всплывают в проде:
- Место на разделе с
/tmp. Если/tmp— отдельный маленький раздел или tmpfs в оперативке, большой файл его переполнит. Проверьтеdf -hи вынеситеupload_tmp_dirна диск с запасом. - Права на директорию. Процесс FPM (обычно
www-data) должен иметь право писать и вupload_tmp_dir, и в целевую папку. Иначеmove_uploaded_fileвернётfalseбез исключения. - Атомарность. Не пишите в финальный путь напрямую, если файл кто-то может прочитать в процессе. Пишите во временное имя рядом и делайте
rename— на одной ФС это атомарная операция.
Итоговый чек-лист перед релизом
- Nginx:
client_max_body_size≥ максимального файла с запасом.nginx -tпрошёл. - PHP:
post_max_size>upload_max_filesize, оба покрывают ваш максимум. - Код проверяет
$_FILES['field']['error']и случай пустого$_FILESпри непустомCONTENT_LENGTH. - Нигде нет
file_get_contents/fileпо файлу неизвестного размера — только потоковое чтение. - Отдача на скачивание идёт через
X-Accel-Redirectили с очищенным буфером вывода. - Таймауты Nginx, PHP-FPM и
max_execution_timeсогласованы под реальное время обработки. - На разделе с
upload_tmp_dirи целевой папкой есть место (df -h) и права уwww-data. - Финальная запись атомарна: временное имя плюс
rename.
Что настроить дальше
Для файлов от нескольких сотен мегабайт классическая форма — плохой UX: один обрыв связи, и всё заново. Смотрите в сторону chunked-загрузки на фронте (tus, Resumable.js), когда файл режется на части и склеивается на сервере — тогда лимиты PHP на один запрос перестают вас держать, а прерванную загрузку можно продолжить. Инвалидация и очистка залежавшихся временных файлов — отдельная тема, здесь не разбираем. В проде поставьте на мониторинг размер upload_tmp_dir и число 413/504 в логах Nginx: рост этих метрик покажет, что вы упёрлись в лимит раньше, чем это увидят пользователи.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: PHP Manual: Handling file uploads, Nginx: ngx_http_core_module (client_max_body_size)
Частые вопросы
Я поднял upload_max_filesize, но файл всё равно не грузится. Почему?
У меня PHP 7.4, а не 8.x — настройки те же?
Скрипт падает с memory_exhausted, хотя я поднял memory_limit до 512M. Что не так?
Загрузка обрывается через пару минут с ошибкой 504. Куда смотреть?
Нужно ли отдавать большие файлы на скачивание через PHP?
Как понять актуальные лимиты на работающем сервере, не лазая в конфиги?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.