Загрузка файлов в Django: сравнение четырёх подходов
Один и тот же аплоад в Django можно реализовать четырьмя способами — от простого FileField до отдачи через presigned URL. У каждого своя цена: память, время воркера, счёт за трафик.

Пользователь заливает видео на 800 МБ, и ваш воркер Gunicorn держит соединение всё время загрузки, а память сервера пухнет. Или бэкенд честно принимает файл, сохраняет на диск — а через месяц диск на инстансе кончается и приложение падает на записи. Загрузка файлов в Django выглядит как одна строка FileField, но за этой строкой прячется минимум четыре разных решения с разной ценой по памяти, трафику и нагрузке на воркеры. Разберём их для Django 5.x.
Смежные темы — валидация типов файлов и защита от вредоносных загрузок — здесь не разворачиваем, это отдельный разговор. Фокус только на том, как байты доходят от клиента до хранилища и кто за это платит.
Что вообще происходит при загрузке
Django принимает multipart/form-data и не грузит весь файл в память сразу. За это отвечают загрузчики (upload handlers). По умолчанию их два: MemoryFileUploadHandler и TemporaryFileUploadHandler. Порог переключения задаёт настройка FILE_UPLOAD_MAX_MEMORY_SIZE — по умолчанию 2,5 МБ.
Всё, что меньше порога, остаётся в оперативной памяти как InMemoryUploadedFile. Всё, что больше, пишется во временный файл на диске как TemporaryUploadedFile. Это важно: если у вас грузят гигабайтные архивы, они не съедят RAM целиком — но временный файл всё равно проходит через ваш воркер.
Пока байты идут через ваш Python-процесс, этот процесс занят. Один загружающий гигабайт пользователь на медленном канале держит воркер целую минуту — и в это время он не обслуживает никого другого.
Отсюда растут все различия между подходами: вопрос в том, проходят ли байты через ваш код и где они в итоге лежат.
Подход 1. FileField и локальный диск
Базовый вариант из документации. Модель с FileField, форма, файл сохраняется в директорию MEDIA_ROOT.
# models.py — Django 5.x
from django.db import models
class Document(models.Model):
title = models.CharField(max_length=200)
# upload_to принимает функцию или строку с шаблоном даты
file = models.FileField(upload_to="documents/%Y/%m/")
uploaded_at = models.DateTimeField(auto_now_add=True)
# views.py
from django.shortcuts import render, redirect
from .forms import DocumentForm
def upload_document(request):
if request.method == "POST":
form = DocumentForm(request.POST, request.FILES)
if form.is_valid():
form.save() # файл пишется в MEDIA_ROOT, путь — в БД
return redirect("upload_done")
else:
form = DocumentForm()
return render(request, "upload.html", {"form": form})
Цена. Просто и работает из коробки. Но файлы лежат на локальном диске инстанса: при горизонтальном масштабировании их не видят соседние инстансы, при пересоздании контейнера они пропадают. Отдачу медиа в проде вешают на Nginx, а не на Django. Подходит для одного сервера и небольших файлов.
Подход 2. FileField поверх S3-совместимого хранилища
Тот же FileField, но бэкенд хранилища — облако. С Django 4.2 хранилища настраиваются через словарь STORAGES, а не через устаревший DEFAULT_FILE_STORAGE. Для работы с S3 обычно берут пакет django-storages вместе с boto3.
# settings.py — Django 4.2+ / 5.x, django-storages
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
# Параметры бэкенда S3 — сверяйте актуальные ключи
# в документации django-storages вашей версии
AWS_STORAGE_BUCKET_NAME = "my-bucket"
AWS_S3_REGION_NAME = "ru-central1"
# ключи доступа держите в переменных окружения, не в коде
# AWS_ACCESS_KEY_ID = os.environ["YOUR_ACCESS_KEY"]
Модель не меняется вообще: form.save() теперь кладёт файл в бакет. Цена. Файлы переживают пересоздание инстанса и видны всем воркерам. Но байты по-прежнему идут через ваш процесс: клиент → ваш воркер → S3. Воркер занят на всё время загрузки, а вы платите ещё и за исходящий трафик из приложения в хранилище.
Подход 3. Стриминг больших файлов и настройка загрузчиков
Когда файлы крупные, разумно управлять поведением загрузчиков. Можно поднять порог памяти или, наоборот, заставить всё сразу писать во временный файл, чтобы не рисковать RAM.
# views.py — принудительно писать во временный файл на диск
from django.core.files.uploadhandler import TemporaryFileUploadHandler
def upload_large(request):
if request.method == "POST":
# переопределяем список обработчиков для этого запроса
request.upload_handlers = [TemporaryFileUploadHandler(request)]
# дальше — обычная обработка request.FILES
# ...
Важная деталь: подменять request.upload_handlers нужно до первого обращения к request.POST или request.FILES. Если Django уже начал разбирать тело запроса, вы получите AttributeError. Из-за этого CSRF-проверка через декоратор мешает — она читает POST раньше. Порядок настройки обработчиков описан в разделе документации про upload handlers.
Цена. Контроль над памятью и диском, но байты всё равно проходят через воркер. Это решение для случая «файлы большие, но загрузок немного». Оно не убирает главную проблему — занятый воркер.
Подход 4. Прямая загрузка в хранилище через presigned URL
Самый радикальный вариант: файл вообще не касается вашего бэкенда. Django генерирует временную подписанную ссылку, браузер грузит файл напрямую в S3, а вам присылает только итоговый ключ объекта.
# views.py — выдаём presigned URL для прямой загрузки
import boto3
from django.http import JsonResponse
def get_upload_url(request):
object_key = f"uploads/{request.user.id}/{request.GET['filename']}"
s3 = boto3.client("s3", region_name="ru-central1")
# ссылка живёт ограниченное время (в секундах)
presigned = s3.generate_presigned_url(
"put_object",
Params={"Bucket": "my-bucket", "Key": object_key},
ExpiresIn=300,
)
return JsonResponse({"url": presigned, "key": object_key})
Клиент делает PUT на полученный URL, минуя Django. После успеха фронтенд отправляет вам object_key, вы сохраняете его в БД как обычную строку. Цена. Воркер свободен, трафик не идёт через ваш сервер, масштабируется до любого размера файлов. Но валидацию содержимого до записи вы уже не сделаете простым способом — проверять придётся постфактум или через ограничения самого хранилища. И фронтенд усложняется: нужен JS, который запросит ссылку и выполнит прямую загрузку.
Сравнение
| Критерий | Локальный диск | FileField + S3 | Стриминг на диск | Presigned URL |
|---|---|---|---|---|
| Байты идут через воркер | Да | Да | Да | Нет |
| Переживает пересоздание инстанса | Нет | Да | Нет | Да |
| Расход RAM на файл | До порога | До порога | Минимальный | Нулевой |
| Простота валидации содержимого | Высокая | Высокая | Высокая | Низкая |
| Нужен JS на фронте | Нет | Нет | Нет | Да |
| Масштаб файлов | Малый | Средний | Средний | Любой |
Когда что выбирать
- Локальный диск — прототип, внутренний инструмент, один сервер, файлы до пары мегабайт. Не тащите сюда S3, пока не упёрлись.
- FileField + S3 — стандарт для большинства продовых приложений с несколькими инстансами и файлами до десятков мегабайт: аватарки, документы, картинки товаров.
- Стриминг на диск — редкие загрузки крупных файлов, где вам важна серверная валидация и вы готовы держать воркер занятым.
- Presigned URL — видео, крупные архивы, большой поток загрузок. Когда занятый воркер становится узким местом, это единственный путь.
Что ломается на практике
- Таймаут прокси. Nginx и балансировщики режут долгие запросы (часто по умолчанию 60 секунд). Большой файл на медленном канале не долетит через воркер — либо повышайте таймаут, либо уходите на presigned URL.
- Лимит размера тела. У Nginx есть
client_max_body_size(по умолчанию 1 МБ), у Django —DATA_UPLOAD_MAX_MEMORY_SIZE. Оба надо поднять согласованно, иначе получите 413 ещё до Python. - Подмена upload_handlers после чтения POST. Классическая ошибка из подхода 3 — обращение к
request.POSTраньше замены обработчиков. - CORS для presigned URL. Прямая загрузка из браузера в бакет требует настроенного CORS на стороне хранилища, иначе
PUTупрётся в политику браузера.
Что делать дальше
Определите свой потолок: замерьте средний и максимальный размер загружаемого файла и частоту загрузок в пике. Если суммарное время, которое воркеры проводят на приёме файлов, начинает влиять на латентность остальных запросов — это сигнал переходить на presigned URL. Смотрите в проде на два показателя: длительность запросов на аплоад-эндпоинте и потребление памяти воркерами под нагрузкой. Следующий шаг после выбора подхода — валидация загружаемых файлов, но это отдельная тема.
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: Django documentation — File Uploads, django-storages documentation
Частые вопросы
У меня Django 3.2, там тоже словарь STORAGES?
Загрузка большого файла падает по таймауту, что делать?
Можно ли валидировать тип файла до сохранения?
Presigned URL сгенерировался, но браузер выдаёт ошибку CORS.
Файлы пропадают после передеплоя, хотя код не менялся.
Как понять, что мой процесс упирается в приём файлов?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.