Загрузка больших файлов в PHP: сравниваем четыре подхода
Пользователь загружает видео на 800 МБ, PHP отдаёт 500 или обрывает соединение на середине. Разбираем, почему прямой upload не тянет большие файлы и какие четыре подхода решают проблему.

Форма загрузки работает на тестовых картинках по 200 КБ и падает, как только менеджер пробует залить видеоролик на 800 МБ. В логах — POST Content-Length exceeds the limit или обрыв на 40% с таймаутом Nginx. Причина не в одной настройке, а в цепочке лимитов: веб-сервер, PHP, память процесса и время выполнения. Разберём четыре подхода к загрузке больших файлов и цену каждого — от простого поднятия лимитов до чанковой загрузки с докачкой.
Почему обычный upload ломается на больших файлах
Стандартная форма <input type="file"> отправляет весь файл одним POST-запросом. На пути этого запроса стоят четыре ограничения, и упереться можно в любое:
- Nginx / Apache —
client_max_body_size(по умолчанию в Nginx 1 МБ). Превышение даёт413 Request Entity Too Largeещё до PHP. - PHP: размер тела —
upload_max_filesizeиpost_max_size. Второй должен быть больше первого, иначе$_FILESпридёт пустым без явной ошибки. - PHP: время —
max_input_timeиmax_execution_time. На медленном канале файл физически не успевает долиться. - PHP: память — при обработке через
$_FILESсам файл не грузится в память целиком (лежит во временном файле), но постобработка (например, чтение черезfile_get_contents) память съедает.
Настройки актуальны для PHP 8.1–8.3. Значения по умолчанию зависят от сборки и дистрибутива, поэтому смотрите вывод php -i | grep upload на своём сервере, а не документацию соседа.
Подход 1. Просто поднять лимиты
Самое очевидное — увеличить все четыре группы лимитов. Для файлов до нескольких сотен мегабайт этого хватает.
; php.ini (PHP 8.x)
upload_max_filesize = 1024M
post_max_size = 1050M ; чуть больше upload_max_filesize
max_input_time = 300
max_execution_time = 300
memory_limit = 256M
# nginx.conf или в блоке server
client_max_body_size 1050m;
# увеличить таймауты на чтение тела запроса
client_body_timeout 300s;
Что выигрываете: пять минут работы, форма остаётся прежней. Чем платите: один долгий запрос держит воркер PHP-FPM всё время загрузки. На медленном мобильном канале файл на 1 ГБ качается минуты — и всё это время процесс занят. Десять таких пользователей одновременно исчерпают пул pm.max_children, и сайт перестанет отвечать остальным. Обрыв связи на 90% означает загрузку заново — докачки нет.
Подход 2. Чанковая загрузка на клиенте
Файл режется на куски по несколько мегабайт средствами браузера (Blob.slice()), и каждый кусок уходит отдельным запросом. Сервер складывает куски в один файл. Это снимает проблему единого гигантского POST и открывает докачку.
Приёмная часть на PHP дописывает пришедший чанк в конец файла:
<?php
// upload-chunk.php — приём одного чанка (PHP 8.x)
// клиент передаёт: uploadId, chunkIndex, totalChunks, filename
$uploadId = preg_replace('/[^a-z0-9\-]/i', '', $_POST['uploadId'] ?? '');
$chunkIndex = (int)($_POST['chunkIndex'] ?? -1);
$totalCount = (int)($_POST['totalChunks'] ?? 0);
if ($uploadId === '' || $chunkIndex < 0 || !isset($_FILES['chunk'])) {
http_response_code(400);
exit(json_encode(['error' => 'bad request']));
}
$tmpDir = sys_get_temp_dir() . '/upload_' . $uploadId;
if (!is_dir($tmpDir)) {
mkdir($tmpDir, 0700, true);
}
// каждый чанк сохраняем отдельным файлом с номером в имени
$dest = $tmpDir . '/' . str_pad((string)$chunkIndex, 6, '0', STR_PAD_LEFT);
move_uploaded_file($_FILES['chunk']['tmp_name'], $dest);
// когда все чанки на месте — склеиваем
$saved = glob($tmpDir . '/*');
if (count($saved) === $totalCount) {
sort($saved);
$final = fopen(__DIR__ . '/uploads/final_' . $uploadId, 'wb');
foreach ($saved as $part) {
// построчная склейка, память не растёт с размером файла
$in = fopen($part, 'rb');
stream_copy_to_stream($in, $final);
fclose($in);
unlink($part);
}
fclose($final);
rmdir($tmpDir);
echo json_encode(['status' => 'complete']);
} else {
echo json_encode(['status' => 'chunk_received', 'index' => $chunkIndex]);
}
Ключевая деталь — склейка через stream_copy_to_stream, а не file_get_contents + конкатенацию: память не зависит от размера файла. Что выигрываете: каждый запрос короткий, воркер освобождается быстро, докачку реализовать реально (клиент спрашивает сервер, какие индексы уже приняты). Чем платите: нужен клиентский JS, отдельная логика на сервере и уборка недокачанных upload_* каталогов по cron — иначе временная папка забьётся мусором от прерванных загрузок.
Чанковая загрузка не ускоряет передачу — она делает её устойчивой. Гигабайт по-прежнему едет столько же, но обрыв на 900-м мегабайте стоит одного чанка, а не всего файла заново.
Подход 3. Прямая загрузка в объектное хранилище (presigned URL)
Файл вообще не проходит через ваш PHP-бэкенд. Сервер выдаёт клиенту подписанную ссылку (presigned URL) на S3-совместимое хранилище, браузер грузит файл напрямую туда, а бэкенду приходит только уведомление об успехе. Для S3 через AWS SDK for PHP ссылка создаётся методом получения presigned request у клиента S3Client — точное имя метода и параметры срока действия сверяйте с документацией используемой версии SDK, они менялись между мажорными релизами.
Что выигрываете: PHP-FPM вообще не занят передачей, трафик не идёт через ваш сервер, хранилище само разбирается с масштабом. Чем платите: инфраструктурная зависимость (нужен S3/MinIO), настройка CORS на бакете, и вы больше не можете проверять содержимое файла в момент загрузки — валидация переезжает на этап после уведомления.
Подход 4. Протокол докачки (tus)
tus — открытый протокол резюмируемой загрузки поверх HTTP. Вместо самописной чанковой логики берётся готовая клиентская библиотека и серверная реализация. Клиент сам режет файл, отслеживает прогресс, докачивает после разрыва. Для PHP есть серверные реализации протокола — конкретный пакет и его совместимость с вашей версией PHP проверяйте в его репозитории, не полагайтесь на память.
Что выигрываете: не изобретаете докачку, протокол проверен. Чем платите: ещё одна зависимость и необходимость понять, как реализация складывает файлы и убирает незавершённые загрузки.
Сравнение подходов
| Критерий | Поднять лимиты | Свои чанки | Presigned URL | tus |
|---|---|---|---|---|
| Время внедрения | минуты | дни | часы–день | часы |
| Докачка после обрыва | нет | да, вручную | частично | да, из коробки |
| Нагрузка на PHP-FPM | высокая | средняя | минимальная | средняя |
| Внешние зависимости | нет | нет | S3/MinIO | библиотека |
| Валидация в момент загрузки | да | да | нет | да |
| Практический потолок | ~1–2 ГБ | десятки ГБ | терабайты | десятки ГБ |
Когда что выбирать
- Файлы до 100–200 МБ, десятки загрузок в день — поднять лимиты. Усложнять нет смысла: обрыв на таком объёме редок и переживаем.
- Файлы в сотни мегабайт, пользователи на мобильном канале, обрывы частые — чанковая загрузка. Свои чанки, если не хотите зависимостей; tus, если готовы взять библиотеку и не писать докачку руками.
- Файлы от гигабайта или высокая параллельность — presigned URL в объектное хранилище. Держать PHP-воркер занятым на время передачи терабайтного файла нельзя.
- Обязательна проверка содержимого до сохранения (антивирус, разбор формата) — исключите presigned: файл придёт в хранилище раньше, чем вы его увидите.
Что ломается на практике
- post_max_size меньше upload_max_filesize.
$_FILESприходит пустым, ошибки нет. Всегда держитеpost_max_sizeбольше — файл плюс накладные расходы формы. - Забыли client_max_body_size в Nginx. PHP-лимиты подняли, а 413 всё равно летит — потому что Nginx режет запрос раньше PHP.
- Склейка чанков через строки.
$data .= file_get_contents($part)на гигабайтном файле упрётся вmemory_limit. Только потоковое копирование. - Незавершённые загрузки не убираются. Прерванные чанковые сессии копятся во временной папке и однажды забивают диск. Нужен cron, удаляющий каталоги старше нескольких часов.
- PHP-FPM таймаут против Nginx таймаута. Даже с большим
max_execution_timeзапрос обрываетfastcgi_read_timeoutв Nginx. Настраивать нужно обе стороны.
С чего начать
Замерьте реальный профиль: какой максимальный размер файла у ваших пользователей и сколько загрузок идёт одновременно в пик. Если это сотни мегабайт и единичные параллельные загрузки — начните с поднятия лимитов, это честное решение, а не костыль. Как только упрётесь в обрывы на мобильном канале или в исчерпание пула FPM — переходите на чанки или presigned URL по таблице выше. Следующий показатель, за которым стоит следить в проде, — среднее время удержания воркера PHP-FPM на upload-эндпоинте: если оно растёт, вы платите воркерами за передачу, и пора выносить загрузку из бэкенда.
Локализатор видео: Перевод и озвучка видео на другой язык с укладкой в исходный тайминг. Глоссарий терминов, отметки проблемных реплик.
Спросить за 250 ₽Источники: PHP Manual: Handling file uploads, tus resumable upload protocol
Частые вопросы
У меня PHP 7.4, а не 8.x — подходы те же?
Поднял все лимиты, но всё равно 413 — почему?
Файл доходит, но $_FILES пустой — что не так?
Чанковая загрузка ускорит передачу больших файлов?
Можно ли проверить файл на вирусы при загрузке в S3 через presigned URL?
Как чистить недокачанные чанки, чтобы не забить диск?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.