Загрузка больших файлов в PHP: чек-лист перед продакшеном
Форма загрузки работает на файле 2 МБ, но падает на 200 МБ с белым экраном или 413. Разбираем, какие лимиты PHP, Nginx и PHP-FPM надо согласовать до выката.

Классическая история: локально форма загрузки принимает тестовый PDF на пару мегабайт, всё зелёное. На проде пользователь пытается залить видео на 200 МБ и получает белый экран, обрыв на 30% или лаконичное 413 Request Entity Too Large. Причина почти всегда одна — несколько независимых лимитов в разных слоях стека выставлены на разные значения, и самый маленький из них рубит запрос первым.
Здесь собран чек-лист того, что нужно проверить перед выкатом: настройки PHP, PHP-FPM, веб-сервера и логика самого приложения. Ориентируюсь на PHP 8.x и связку Nginx + PHP-FPM как самую частую. Для Apache отдельные оговорки будут по ходу.
Где именно рвётся запрос
Загрузка большого файла проходит через цепочку, и на каждом звене свой предел. Понимать порядок важно: если Nginx отрезал запрос по client_max_body_size, до PHP дело вообще не дойдёт, и ковырять php.ini бесполезно.
- Веб-сервер (Nginx/Apache) — первым принимает тело запроса и может отказать по своему лимиту.
- PHP-FPM — свои таймауты на обработку запроса воркером.
- PHP-движок —
upload_max_filesize,post_max_size,memory_limit,max_execution_time,max_input_time. - Приложение — фреймворк или ваш код с собственной валидацией размера.
Директивы PHP, которые надо согласовать
Три директивы связаны жёсткой зависимостью. Правило: memory_limit ≥ post_max_size ≥ upload_max_filesize. Если post_max_size меньше размера тела запроса, PHP молча обнулит $_POST и $_FILES — форма придёт пустой, без явной ошибки.
| Директива | За что отвечает | Типовое значение под 200 МБ файл |
|---|---|---|
upload_max_filesize | Максимальный размер одного загружаемого файла | 200M |
post_max_size | Максимальный размер всего тела POST (файл + поля формы) | 210M |
memory_limit | Память на процесс. Через move_uploaded_file файл не грузится в память целиком, но запас нужен | 256M и выше |
max_execution_time | Время выполнения скрипта, секунды | 300 |
max_input_time | Время на парсинг входных данных, включая приём тела | 300 или -1 |
Проверить фактические значения на сервере (не то, что в файле, а то, что реально применилось):
php -i | grep -E "upload_max_filesize|post_max_size|memory_limit|max_execution_time|max_input_time"Для веб-контекста значения из CLI могут отличаться от FPM — у них часто разные php.ini. Надёжнее посмотреть через phpinfo() на временной странице (потом удалить) либо в конфиге пула FPM.
Чек-лист по слоям
- Nginx. Поднимите
client_max_body_sizeв блокеhttp,serverилиlocation. По умолчанию 1 МБ — этого не хватит ни на что. Значение должно быть не меньшеpost_max_size. - Nginx-таймауты. При медленной загрузке большого файла упирается
client_body_timeout(приём тела) иfastcgi_read_timeout(ожидание ответа от FPM). Держите их не меньшеmax_execution_time. - PHP-FPM. В конфиге пула проверьте
request_terminate_timeout— если он меньше, чемmax_execution_time, воркер убьётся раньше, чем скрипт закончит. - PHP. Согласуйте пять директив из таблицы выше.
- Приложение. Убедитесь, что фреймворк не режет размер своим лимитом (например, свои проверки в middleware).
Пример server-блока Nginx для загрузок:
server {
listen 80;
server_name upload.example.com;
# тело запроса до 210 МБ; согласовать с post_max_size
client_max_body_size 210m;
# время на приём тела от медленного клиента
client_body_timeout 300s;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# ждать ответа от FPM дольше стандартных 60с
fastcgi_read_timeout 300s;
}
}После правки перезагрузите Nginx (nginx -t && systemctl reload nginx) и FPM (systemctl reload php8.2-fpm). Правки php.ini без перезапуска FPM не применятся.
Самая частая потеря времени при отладке: правятphp.iniдля CLI, а веб-запросы идут через FPM с другим конфигом. Всегда сверяйте, какой именноphp.iniподхватил FPM — путь виден в выводеphp-fpm -iили вphpinfo()в строке Loaded Configuration File.
Минимальный рабочий обработчик
Форма и обработчик, которые корректно принимают файл и проверяют код ошибки. Ключевое — проверять $_FILES['file']['error'], а не только наличие файла: при превышении лимита PHP файл в массив попадёт, но с кодом ошибки.
<?php
// upload.php — PHP 8.x
declare(strict_types=1);
$targetDir = __DIR__ . '/uploads';
$maxBytes = 200 * 1024 * 1024; // 200 МБ, дублируем лимит на уровне кода
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Only POST');
}
// Если тело превысило post_max_size, $_FILES будет пустым
if (empty($_FILES) && ($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
http_response_code(413);
exit('Файл превышает post_max_size, форма пришла пустой');
}
$file = $_FILES['file'] ?? null;
if ($file === null) {
http_response_code(400);
exit('Файл не передан');
}
// Разбор кода ошибки загрузки
switch ($file['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_INI_SIZE:
http_response_code(413);
exit('Превышен upload_max_filesize');
case UPLOAD_ERR_FORM_SIZE:
http_response_code(413);
exit('Превышен MAX_FILE_SIZE из формы');
case UPLOAD_ERR_PARTIAL:
http_response_code(400);
exit('Файл загружен частично (обрыв соединения)');
default:
http_response_code(500);
exit('Ошибка загрузки, код ' . $file['error']);
}
if ($file['size'] > $maxBytes) {
http_response_code(413);
exit('Файл больше допустимого');
}
// Имя генерируем сами, оригинальное не доверяем
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$name = bin2hex(random_bytes(16)) . ($ext !== '' ? '.' . $ext : '');
$dest = $targetDir . '/' . $name;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
http_response_code(500);
exit('Не удалось сохранить файл');
}
http_response_code(201);
echo 'OK: ' . $name;Обратите внимание на две защиты: проверку UPLOAD_ERR_INI_SIZE (сработает при превышении upload_max_filesize) и обязательную генерацию собственного имени файла вместо доверия к $_FILES['file']['name'], которое приходит от клиента и может содержать путь или опасные символы.
Проверка типа файла
Не полагайтесь на MIME из $_FILES['file']['type'] — его подставляет браузер, и его легко подделать. Определяйте тип по содержимому через расширение fileinfo:
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
// сравниваем $mime с белым списком, а не с $file['type']Если не получилось
- 413 Request Entity Too Large — режет Nginx. Поднимите
client_max_body_size. До PHP запрос не дошёл. - Белый экран,
$_POSTи$_FILESпустые — тело превысилоpost_max_size. PHP отбросил данные без исключения. Ловите это условием на пустой$_FILESпри ненулевомContent-Length, как в коде выше. - Загрузка обрывается на середине, в логе Nginx
upstream timed out— упёрлись вfastcgi_read_timeoutилиrequest_terminate_timeoutFPM. Поднимите оба. - 504 Gateway Timeout — медленный клиент не успел передать тело за
client_body_timeoutлибо FPM убил воркер поrequest_terminate_timeout. - Код ошибки
UPLOAD_ERR_NO_TMP_DIRилиUPLOAD_ERR_CANT_WRITE— нет прав или места во временной директории. Проверьтеupload_tmp_dirи свободное место на разделе/tmp.
Что делать дальше
Для файлов от сотен мегабайт и выше классическая форма — тупиковый путь: один медленный клиент держит воркер FPM целиком, а обрыв соединения означает загрузку заново с нуля. Следующий шаг — chunked-загрузка на стороне клиента (библиотеки вроде tus или resumable-протоколы), при которой файл режется на части и собирается на сервере. Инвалидация и сборка чанков — отдельная тема, здесь мы разобрали только пороги для монолитной загрузки.
Перед выкатом мониторьте два показателя: время удержания воркеров FPM (request duration в статусе пула) и заполнение раздела с upload_tmp_dir — незавершённые загрузки любят накапливать мусор во временной папке.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: PHP Manual — Handling file uploads, Nginx — ngx_http_core_module (client_max_body_size)
Частые вопросы
У меня PHP 7.4, а не 8.x — код и настройки подойдут?
Правлю php.ini, но значения не меняются. Почему?
Что если файл больше post_max_size — будет ли исключение?
Достаточно ли проверять $_FILES['file']['type'] для контроля формата?
Загрузка работает, но periodически падает на больших файлах у части пользователей. Куда смотреть?
Как задать лимиты, если у меня Apache, а не Nginx?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.