Большие загрузки и тяжёлая выдача файлов — одни из самых «кинжальных» сценариев для веб-сервера. Здесь всплывают 413 Request Entity Too Large, 499 и 504 таймауты, пустые массивы $_FILES
, забитые временные каталоги и «горячие» PHP-процессы, которые зачем-то стримят клипу 8 ГБ пользователю. В этой статье разбираем, как правильно согласовать лимиты Nginx и PHP (client_max_body_size
, upload_max_filesize
, post_max_size
), какие таймауты действительно важны, как организовать безопасную отдачу больших файлов через X-Accel-Redirect
и сделать так, чтобы пользователи могли докачивать файлы (resume downloads) через Range-запросы.
Как проходит загрузка: краткая схема Nginx + PHP-FPM
Клиент отправляет POST multipart/form-data
на ваш сайт. Nginx принимает тело запроса и складывает его во временные файлы (по частям), затем передаёт запрос в PHP-FPM (через FastCGI). PHP читает данные из файлов/потока и раскладывает их в $_FILES
. Если по пути что-то «узкое» — запрос отклоняется или рвётся по таймауту.
Критично: при несогласованных лимитах тело запроса может быть отброшено ещё на уровне Nginx (413) — до попадания в PHP. Тогда
$_FILES
будет пустым.
Три главных лимита размера
Nginx: client_max_body_size
client_max_body_size
ограничивает общий размер тела запроса. Применяется на ранней стадии. Если превышен — Nginx вернёт 413 без обращения к приложению. Удобно, если вы хотите экономить ресурсы и трафик, но пользователь увидит стандартную страницу ошибки, если не настроить error_page
.
PHP: upload_max_filesize и post_max_size
upload_max_filesize
— максимальный размер одного загружаемого файла. post_max_size
— максимальный общий размер тела POST
, включая все поля и файлы. Обычно post_max_size
должен быть больше или равен upload_max_filesize
с некоторым запасом (пары мегабайт хватает).
Типичные симптомы несогласованности:
- Nginx меньше PHP: пользователь получает 413 мгновенно, приложение не видит запрос.
- PHP меньше Nginx: загрузка идёт долго, а потом приложение сообщает о превышении лимита; ресурсы тратятся больше, зато можно показать дружелюбную ошибку.
Практика: в проектах, где UX важнее экономии трафика, ставьте лимит Nginx чуть больше лимитов PHP, чтобы ошибка обрабатывалась приложением. В API и сильной нагрузке — наоборот, Nginx жёстче режет заранее.

Не только размер: таймауты и буферы
Даже при правильных лимитах большие загрузки могут упираться в таймауты или дисковые ограничения. Вот что важно проверить:
client_body_timeout
(Nginx): время ожидания следующего фрагмента тела запроса. Если пользователь грузит медленно — увеличьте.send_timeout
(Nginx): таймаут отправки ответа клиенту (включая выдачу файлов).keepalive_timeout
(Nginx): полезно, но к загрузке напрямую не относится.fastcgi_read_timeout
(Nginx): сколько ждать ответа от PHP. Большой парсинг/обработка — увеличьте осторожно.client_body_buffer_size
и tmp-path-и Nginx: куда и как буферизуется тело запроса до передачи в бэкенд.max_execution_time
,max_input_time
(PHP): ограничивают длительность обработки/чтения входа.memory_limit
(PHP): не относится напрямую к файлу, но может «сбросить» скрипт при неосторожном чтении в память.upload_tmp_dir
(PHP): временный каталог для загружаемых файлов; проверьте квоты/прав доступа/свободное место.
Если нужно тонко управлять параметрами и I/O под нагрузкой, удобнее делать это на VDS. Если вы обслуживаете проект на шаред-площадке — уточните доступные лимиты у провайдера или используйте наш виртуальный хостинг с актуальными сборками PHP и Nginx.
Где хранится тело запроса: временные каталоги
При больших загрузках тело запроса буферизуется во временные файлы. Две точки отказа — нехватка места и права:
- Nginx:
client_body_temp_path
(по умолчанию каталог temp внутри префикса Nginx). Должен лежать на файловой системе с достаточным свободным местом и скоростью. - PHP:
upload_tmp_dir
. Если не задан, используется системный TMP. На хостингах часто ограничен квотой.
Если места мало — загрузки «случайно» обрываются, в логах Nginx/FPM будут ошибки наподобие «No space left on device» или проблемы с перемещением файла (move_uploaded_file
возвращает false).
Базовый чек-лист настроек
- Определите максимально допустимый размер загружаемых файлов и общий размер формы.
- Выберите стратегию отказа (ранний 413 в Nginx или обработка на уровне приложения).
- Согласуйте
client_max_body_size
,post_max_size
,upload_max_filesize
. - Проверьте
client_body_timeout
иfastcgi_read_timeout
. - Назначьте и проверьте каталоги временных файлов в Nginx и PHP, следите за диском и inode.
- Убедитесь, что ulimit и системные лимиты файлов/сокетов достаточны под пиковую нагрузку.
Пример конфигурации Nginx для больших загрузок
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
client_max_body_size 1024m; # 1 ГБ
client_body_timeout 120s;
send_timeout 120s;
# При необходимости перенесите temp в отдельный быстрый раздел
# client_body_temp_path /var/lib/nginx/body 1 2;
}
server {
listen 80;
server_name example.local;
# Кастомная страница для 413
error_page 413 = /413.html;
location = /413.html { internal; }
location / {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_read_timeout 180s;
}
}
PHP (php.ini) под крупные загрузки
file_uploads = On
upload_max_filesize = 1024M
post_max_size = 1026M
max_file_uploads = 50
max_input_time = 180
max_execution_time = 180
memory_limit = 512M
; upload_tmp_dir = /path/to/php-tmp ; при необходимости вынесите на отдельный том
Частая ошибка — поставить upload_max_filesize
больше post_max_size
. В таком случае загрузка «сломается» тихо: файл будто бы игнорируется, а $_FILES
пустой. Обязательно проверяйте корректность значений и единиц (M/G vs Mb/Gb).

Проверка через curl
Полезно проверять лимиты без фронтенда:
# Отправить файл и проверить код ответа
curl -i -X POST \
-F "file=@./big.bin" \
http://example.local/upload
# Проверить, поддерживает ли сервер докачку (Range)
curl -I http://example.local/download/big.iso
curl -r 0-1048575 -o part.bin http://example.local/download/big.iso
Почему большие файлы нельзя отдавать через PHP
Технически можно вызвать readfile()
и написать цикл с fpassthru()
. Но это перегружает PHP-FPM: процесс будет занят десятками секунд или минут, упираясь в send_timeout
и сетевую скорость клиента. К тому же PHP хуже поддерживает Range-запросы и докачку «из коробки»; нужно вручную ставить заголовки, следить за буферами, отключать output_buffering, избегать случайного вывода и пр.
Оптимальный путь — переложить выдачу файла на Nginx через внутренний редирект X-Accel-Redirect
. Так Nginx сам будет читать файл с диска, эффективно буферизовать и корректно работать с Range-запросами и медленными клиентами, не блокируя PHP.
Схема X‑Accel‑Redirect
- Файлы хранятся вне веб-корня или в защищённом каталоге, недоступном напрямую.
- PHP выполняет авторизацию/аудит, проверяет право доступа к файлу.
- PHP возвращает пользователю обычный ответ, но добавляет заголовок
X-Accel-Redirect
с внутренним путём, понятным Nginx. - Nginx перехватывает заголовок и отдаёт файл напрямую, применяя свои оптимизации.
Nginx: защищённое внутреннее хранилище
server {
listen 80;
server_name example.local;
# Внутренний location для X-Accel-Redirect
location /protected/ {
internal;
alias /data/protected/; # файлы лежат вне веб-корня
# Опционально ограничить скорость
# limit_rate_after 5m;
# limit_rate 1m;
}
location /download {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/download.php;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
}
PHP: заголовки и редирект для Nginx
<?php
// download.php
// 1) Авторизация и проверка доступа к файлу
$userId = 123; // пример
$filePath = "/data/protected/reports/report-{$userId}.pdf";
if (!is_file($filePath)) {
http_response_code(404);
exit('Not found');
}
$publicName = 'report.pdf';
$internalUri = '/protected/' . basename($filePath);
$filesize = filesize($filePath);
// 2) Заголовки ответа (важно: Content-Type/Disposition/Length)
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . $publicName . '"');
header('Content-Length: ' . $filesize);
header('Accept-Ranges: bytes'); // Nginx всё равно поддержит, но явно — понятнее
// 3) Главный трюк: поручаем отдачу Nginx
header('X-Accel-Redirect: ' . $internalUri);
exit;
Обратите внимание: location /protected/
помечен как internal
, значит прямой доступ к нему запрещён. Только приложение может «включать» выдачу через заголовок X-Accel-Redirect
.
Поддержка Range и resume downloads
При отдаче файлов Nginx умеет обрабатывать заголовок Range
и корректно отвечать частями (206 Partial Content
) со всеми нужными заголовками (Accept-Ranges
, Content-Range
). Благодаря этому докачка в большинстве браузеров и менеджеров загрузок работает «из коробки». Если файл отдаётся через X-Accel-Redirect
, поддержку Range обеспечивает сам Nginx, а не PHP.
Для больших файлов полезно включить низкоуровневые оптимизации чтения:
sendfile on;
— отдача напрямую из файлового кэша ядра.tcp_nopush on;
иtcp_nodelay on;
— аккуратная отправка больших блоков.aio on;
— асинхронное чтение (актуально для некоторых ОС и паттернов I/O).
Если нужно сбалансировать сжатие, кеш и производительность PHP на шаред-площадке — посмотрите также разбор оптимизаций в материале про PHP, OPCache и Brotli.
Тонкости буферизации и заголовков
- Content-Length: лучше указывать точный размер, чтобы клиенты корректно показывали прогресс и умели докачивать. При
X-Accel-Redirect
Nginx может сам вычислить длину файла, но явныйContent-Length
в ответе от PHP не навредит. - Content-Type: ставьте корректный MIME; не полагайтесь на дефолтный
application/octet-stream
, если тип известен. - Content-Disposition:
attachment; filename="..."
— для «скачивания как»; аккуратнее с не-ASCII именами (используйтеfilename*
при необходимости). - Кеширование: если файлы статичны, используйте
expires
/cache-control
вinternal
-локации. Для приватного контента ставьтеCache-Control: private
.
Типовые ошибки и их диагностика
413 Request Entity Too Large
Причины: client_max_body_size
меньше фактического размера. Лечится увеличением лимита и/или настройкой кастомной страницы ошибки. Если хотите, чтобы обработка была в приложении, увеличьте лимит Nginx выше лимитов PHP.
Пустой $_FILES
Чаще всего post_max_size
меньше, чем отправленный размер, или закончился диск в upload_tmp_dir
. Проверьте php.ini, права на TMP и логи PHP-FPM. Не забывайте про max_file_uploads
при множественных инпутах.
499/504, «загрузка останавливается»
499
— клиент сам закрыл соединение (часто из-за низкой скорости или потери сети). 504 Gateway Timeout
— истёк fastcgi_read_timeout
при обработке сервером. Увеличьте таймауты, проверьте диск, I/O и сетевые условия. При загрузке с очень медленных линий поднимите client_body_timeout
.
Выдача больших файлов «через PHP» под завязку грузит CPU
Переведите выдачу на X-Accel-Redirect
. Контролируйте скорость и используйте sendfile
. Это резко разгружает PHP-FPM и позволяет обслуживать больше одновременных скачиваний.
Недокачка и «битые» файлы
Проверьте, не перезаписывает ли приложение заголовки, нет ли лишнего вывода до заголовков (BOM, пробелы), и что по пути нет обратного прокси, который режет Range. Для больших файлов лучше исключить gzip на лету.
Логи для анализа больших запросов
Для наблюдения за проблемами полезно логировать длину запроса и потраченное время:
log_format upload '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'$request_length $request_time';
access_log /var/log/nginx/upload.log upload;
$request_length
покажет, насколько большой был запрос, а $request_time
поможет понять, где узкое место (сеть, диск, приложение).
Рекомендации по проектированию
- Сохраняйте загружаемые файлы вне веб-корня и публикуйте их только через
X-Accel-Redirect
с авторизацией. - Ограничивайте максимально допустимые размеры по ролям/эндпоинтам (админка vs публичная форма), не размывайте единственный глобальный лимит.
- Следите за временными каталогами и дисками (метрики, алерты). «Внезапная» нехватка места — частая причина падений загрузок.
- Если фронтенд поддерживает чанковую загрузку, используйте серверную сборку частей — она стабильнее на нестабильных сетях. На сервере всё равно проверьте итоговый размер.
- Для файлов >2–4 ГБ (особенно на сетевых FS) уделите внимание
aio
и RAID/блочному уровню — от этого заметно зависит стабильность выдачи.
Если планируете самостоятельную администрируемую инфраструктуру, посмотрите обзор панелей для управления своим сервером: сравнение панелей для VDS.
Итоги
Чтобы большие загрузки работали стабильно, согласуйте лимиты client_max_body_size
, upload_max_filesize
и post_max_size
, не забудьте про таймауты и временные каталоги, а для выдачи переходите на X-Accel-Redirect
. Так вы обеспечите предсказуемость, снимете нагрузку с PHP-FPM, получите корректную поддержку Range/ресюма и убережёте пользователей от «битых» скачиваний. Не бойтесь жёстко резать слишком большие запросы на уровне Nginx там, где это оправдано, и давайте приложению контролировать UX там, где это важнее.