Uploads de arquivos muito grandes, como vídeos, backups e conjuntos de dados, exigem uma arquitetura diferente de um envio comum por formulário. Em aplicações com Django, arquivos na faixa de até 10GB podem causar consumo excessivo de memória, tempos de espera longos e falhas difíceis de recuperar quando a conexão oscila.
Um tratamento robusto envolve dividir o problema em camadas: estratégia de envio (em partes, direto para nuvem ou retomável), persistência segura no servidor ou armazenamento externo, registro em banco para rastreamento e ajustes de infraestrutura (Nginx e servidor WSGI). A combinação correta reduz risco de travamentos e melhora a confiabilidade do processo.
Por que uploads grandes são um desafio em Django
O Django, por padrão, lida bem com uploads pequenos porque o fluxo de dados termina rapidamente e o impacto em recursos é limitado. Em arquivos muito grandes, o maior risco é o consumo de memória, quando partes do upload acabam sendo mantidas na RAM em vez de irem direto para disco. Outro problema frequente é timeout, que é o tempo máximo que proxy e servidor permitem para uma requisição antes de encerrá-la. Também existe a questão da experiência: uploads longos exigem progresso e, idealmente, capacidade de retomada após falhas.
Além disso, arquivos grandes pressionam o armazenamento, pois podem encher discos rapidamente e degradar desempenho de I/O (leitura e escrita). O caminho de armazenamento precisa ser pensado para evitar competição com o próprio servidor da aplicação. Por fim, há preocupações de segurança, como validação de tipo de arquivo e controle de abuso por volume. Uma solução completa trata cada uma dessas áreas de forma coordenada.
Estratégia 1: Upload em partes (chunked upload) com remontagem no servidor
O upload em partes divide um arquivo em blocos menores (por exemplo, 5MB) e envia esses blocos em múltiplas requisições. Isso reduz o risco de timeouts por requisição e permite retomar o envio a partir do último bloco concluído. No backend, cada parte é armazenada temporariamente e, ao final, ocorre a remontagem em um único arquivo. Essa estratégia funciona bem em armazenamento local e também pode ser adaptada para nuvem.
Antes do código, é útil entender o papel dos metadados enviados junto de cada parte: um identificador de upload (uploadId) agrupa as partes, o índice indica a ordem, e o total de partes permite estimar progresso e detectar finalização. A seguir está um exemplo completo de envio em partes no navegador com JavaScript.
function gerarIdUnico() {
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function obterCookie(nome) {
const valor = `; ${document.cookie}`;
const partes = valor.split(`; ${nome}=`);
if (partes.length === 2) return partes.pop().split(';').shift();
return null;
}
function obterCsrfToken() {
return obterCookie('csrftoken');
}
function atualizarProgresso(porcentagem) {
// Implementação propositalmente simples
const barra = document.getElementById('barra-progresso');
if (barra) barra.value = porcentagem;
}
async function uploadArquivoGrande(arquivo) {
const tamanhoParte = 5 * 1024 * 1024; // 5MB
const totalPartes = Math.ceil(arquivo.size / tamanhoParte);
const uploadId = gerarIdUnico();
for (let indiceParte = 0; indiceParte < totalPartes; indiceParte++) {
const inicio = indiceParte * tamanhoParte;
const fim = Math.min(inicio + tamanhoParte, arquivo.size);
const parte = arquivo.slice(inicio, fim);
const formData = new FormData();
formData.append('file', parte);
formData.append('chunkIndex', String(indiceParte));
formData.append('totalChunks', String(totalPartes));
formData.append('uploadId', uploadId);
formData.append('fileName', arquivo.name);
const resposta = await fetch('/api/upload-chunk/', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': obterCsrfToken()
}
});
if (!resposta.ok) {
throw new Error(`Falha ao enviar a parte ${indiceParte}`);
}
const progresso = ((indiceParte + 1) / totalPartes) * 100;
atualizarProgresso(progresso);
}
const finalizar = await fetch('/api/finalize-upload/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': obterCsrfToken()
},
body: JSON.stringify({ uploadId, fileName: arquivo.name })
});
if (!finalizar.ok) {
throw new Error('Falha ao finalizar o upload');
}
return finalizar.json();
}
No Django, as partes precisam ser gravadas em disco com nomes determinísticos, dentro de um diretório temporário por uploadId. O handler de upload do Django fornece um objeto de arquivo que pode ser iterado em subpartes por meio de chunks(), evitando carregar tudo em memória. Ao final, uma rota de finalização concatena as partes em ordem e remove os temporários. A seguir está um backend completo e funcional com essas responsabilidades separadas.
import os
import json
import shutil
from pathlib import Path
from django.conf import settings
from django.http import JsonResponse, HttpResponseBadRequest
from django.views.decorators.http import require_http_methods
UPLOAD_TEMP_DIR = Path(settings.MEDIA_ROOT) / 'temp_uploads'
FINAL_UPLOAD_DIR = Path(settings.MEDIA_ROOT) / 'uploads'
def _garantir_diretorio(caminho: Path) -> None:
caminho.mkdir(parents=True, exist_ok=True)
def _validar_campos_chunk(request):
if 'file' not in request.FILES:
return None, HttpResponseBadRequest('Campo "file" ausente.')
campos = ['chunkIndex', 'totalChunks', 'uploadId', 'fileName']
for campo in campos:
if request.POST.get(campo) is None:
return None, HttpResponseBadRequest(f'Campo "{campo}" ausente.')
try:
chunk_index = int(request.POST['chunkIndex'])
total_chunks = int(request.POST['totalChunks'])
except ValueError:
return None, HttpResponseBadRequest('Índices de chunk inválidos.')
upload_id = request.POST['uploadId']
file_name = request.POST['fileName']
return {
'chunk': request.FILES['file'],
'chunk_index': chunk_index,
'total_chunks': total_chunks,
'upload_id': upload_id,
'file_name': file_name,
}, None
@require_http_methods(["POST"])
def upload_chunk(request):
dados, erro = _validar_campos_chunk(request)
if erro:
return erro
_garantir_diretorio(UPLOAD_TEMP_DIR)
pasta_upload = UPLOAD_TEMP_DIR / dados['upload_id']
_garantir_diretorio(pasta_upload)
caminho_parte = pasta_upload / f"chunk_{dados['chunk_index']:06d}"
with open(caminho_parte, 'wb+') as destino:
for pedaco in dados['chunk'].chunks():
destino.write(pedaco)
return JsonResponse({
'status': 'success',
'chunkIndex': dados['chunk_index'],
'uploadId': dados['upload_id']
})
def _montar_arquivo_final(upload_id: str, file_name: str) -> Path:
pasta_upload = UPLOAD_TEMP_DIR / upload_id
if not pasta_upload.exists():
raise FileNotFoundError('Upload temporário não encontrado.')
_garantir_diretorio(FINAL_UPLOAD_DIR)
caminho_final = FINAL_UPLOAD_DIR / file_name
partes = sorted(pasta_upload.glob('chunk_*'))
if not partes:
raise FileNotFoundError('Nenhuma parte encontrada para montagem.')
with open(caminho_final, 'wb') as arquivo_final:
for caminho_parte in partes:
with open(caminho_parte, 'rb') as parte:
shutil.copyfileobj(parte, arquivo_final, length=1024 * 1024)
return caminho_final
@require_http_methods(["POST"])
def finalize_upload(request):
try:
payload = json.loads(request.body.decode('utf-8'))
except json.JSONDecodeError:
return HttpResponseBadRequest('JSON inválido.')
upload_id = payload.get('uploadId')
file_name = payload.get('fileName')
if not upload_id or not file_name:
return HttpResponseBadRequest('Campos "uploadId" e "fileName" são obrigatórios.')
try:
caminho_final = _montar_arquivo_final(upload_id, file_name)
except Exception as exc:
return JsonResponse({'status': 'error', 'message': str(exc)}, status=400)
# Limpeza dos temporários
pasta_upload = UPLOAD_TEMP_DIR / upload_id
if pasta_upload.exists():
shutil.rmtree(pasta_upload)
# Persistência em banco pode ser feita aqui
from .models import UploadedFile
objeto = UploadedFile.objects.create(
original_name=file_name,
size=caminho_final.stat().st_size,
status='completed'
)
return JsonResponse({
'status': 'success',
'fileId': objeto.id,
'message': 'Upload finalizado com sucesso.'
})
Uma preocupação importante nessa estratégia é a integridade: a montagem acima concatena arquivos encontrados, mas um sistema mais rigoroso também registra o total esperado e valida se todas as partes existem. Também é comum prevenir colisões de nomes no destino final, gerando um nome interno único e guardando o nome original apenas como metadado. Essa camada reduz perdas e facilita suporte operacional em caso de falhas.
Modelo de banco para rastrear uploads e estados
Uploads grandes se beneficiam de um registro de estado, pois o processo pode durar minutos e pode haver etapas posteriores como varredura antivírus e transcodificação. Esse registro também ajuda a diferenciar um upload em andamento de um upload concluído, e permite auditoria. Um campo de tamanho em bytes deve usar BigInteger para suportar arquivos muito grandes. Um identificador (upload_id) facilita correlacionar requisições e diretórios temporários.
A seguir está um modelo simples que cobre estados típicos e mantém datas relevantes. Os estados são apenas uma convenção; o valor principal é padronizar o ciclo de vida do arquivo. Em produção, costuma-se adicionar também o caminho no armazenamento final e metadados como tipo MIME e hash. O exemplo abaixo mantém o essencial para rastreamento do fluxo.
from django.db import models
from django.contrib.auth.models import User
class UploadedFile(models.Model):
STATUS_CHOICES = [
('uploading', 'Enviando'),
('processing', 'Processando'),
('completed', 'Concluído'),
('failed', 'Falhou'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
original_name = models.CharField(max_length=255)
size = models.BigIntegerField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='uploading')
upload_id = models.CharField(max_length=100, unique=True, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"{self.original_name} - {self.status}"
Estratégia 2: Upload direto para nuvem com URL pré-assinada
No upload direto para nuvem, o arquivo não passa pelo servidor Django, indo do navegador para um provedor de armazenamento como AWS S3. Isso reduz carga de CPU, RAM e disco do servidor de aplicação, e evita gargalos de rede na instância onde o Django roda. O Django fica responsável por autorizar o envio e emitir uma URL pré-assinada, que é uma autorização temporária para upload com regras específicas. Esse desenho costuma ser o mais escalável para arquivos de muitos gigabytes.
A pré-assinatura também pode impor condições, como limite de tamanho e tipo de conteúdo. Uma forma comum é usar “presigned POST”, que retorna uma URL e campos que o navegador deve enviar via formulário multipart. A seguir está uma função em Python usando boto3 para gerar essa autorização com limite de 10GB. As credenciais e nomes de bucket ficam no settings para evitar valores espalhados pelo código.
import boto3
from botocore.config import Config
from django.conf import settings
def gerar_presigned_post_s3(nome_arquivo: str, tipo_arquivo: str, expiracao_segundos: int = 3600):
cliente = boto3.client(
's3',
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
region_name=settings.AWS_REGION,
config=Config(signature_version='s3v4')
)
chave = f"uploads/{nome_arquivo}"
presigned = cliente.generate_presigned_post(
Bucket=settings.AWS_STORAGE_BUCKET_NAME,
Key=chave,
Fields={'Content-Type': tipo_arquivo},
Conditions=[
{'Content-Type': tipo_arquivo},
['content-length-range', 1, 10 * 1024 * 1024 * 1024] # 10GB
],
ExpiresIn=expiracao_segundos
)
return presigned
O endpoint Django expõe esses dados em JSON para o frontend, que então envia diretamente ao S3. Como essa resposta concede permissão temporária, é comum atrelar a emissão a autenticação e regras de negócio, evitando que usuários não autorizados gerem autorizações ilimitadas. Também é possível incluir um nome interno único para evitar colisões. A seguir está um endpoint simples que entrega URL e campos necessários.
from django.http import JsonResponse, HttpResponseBadRequest
from django.views.decorators.http import require_http_methods
from .utils import gerar_presigned_post_s3
@require_http_methods(["GET"])
def get_upload_url(request):
nome_arquivo = request.GET.get('fileName')
tipo_arquivo = request.GET.get('fileType')
if not nome_arquivo or not tipo_arquivo:
return HttpResponseBadRequest('Parâmetros "fileName" e "fileType" são obrigatórios.')
presigned = gerar_presigned_post_s3(nome_arquivo, tipo_arquivo)
return JsonResponse({
'uploadUrl': presigned['url'],
'fields': presigned['fields']
})
No frontend, o envio ocorre com FormData contendo todos os campos retornados e o arquivo em si. Esse modelo elimina a necessidade de remontagem no servidor, mas ainda pode precisar de registro em banco após o upload para indicar que o arquivo existe e pode ser processado. Em muitos casos, a confirmação é feita chamando o Django após a conclusão do POST para gravar metadados. A seguir está um exemplo completo do envio direto usando fetch.
async function uploadDiretoParaS3(arquivo) {
const consulta = new URLSearchParams({
fileName: arquivo.name,
fileType: arquivo.type || 'application/octet-stream'
});
const resposta = await fetch(`/api/get-upload-url/?${consulta.toString()}`);
if (!resposta.ok) {
throw new Error('Falha ao obter autorização de upload.');
}
const { uploadUrl, fields } = await resposta.json();
const formData = new FormData();
for (const [chave, valor] of Object.entries(fields)) {
formData.append(chave, valor);
}
formData.append('file', arquivo);
const envio = await fetch(uploadUrl, {
method: 'POST',
body: formData
});
if (!envio.ok) {
throw new Error('Falha ao enviar arquivo para o armazenamento.');
}
return { status: 'success' };
}
Estratégia 3: Upload retomável com o protocolo TUS
Uploads retomáveis lidam melhor com redes instáveis, pois o servidor mantém um registro do quanto já foi recebido e permite continuar sem recomeçar do zero. O TUS é um padrão aberto para esse tipo de upload, definindo endpoints e cabeçalhos para envio parcial com retomada confiável. Em Django, uma alternativa é usar um pacote que implementa o protocolo e cuida do armazenamento temporário e do controle de offsets (posição atual do envio). Esse caminho reduz código próprio e tende a ser mais sólido para retomada real.
A configuração envolve definir diretórios de upload e limite máximo de tamanho, além de incluir as rotas. O diretório de upload TUS recebe os dados parciais e o diretório de destino guarda o arquivo final após conclusão. O limite de 10GB é configurado em bytes para evitar ambiguidades. A seguir está um exemplo de configuração típica no settings e urls.
import os
from pathlib import Path
MEDIA_ROOT = Path(__file__).resolve().parent.parent / 'media'
INSTALLED_APPS = [
# ...
'django_tus',
]
TUS_UPLOAD_DIR = os.path.join(MEDIA_ROOT, 'tus_uploads')
TUS_DESTINATION_DIR = os.path.join(MEDIA_ROOT, 'uploads')
TUS_MAX_FILE_SIZE = 10 * 1024 * 1024 * 1024 # 10GB
TUS_FILE_NAME_FORMAT = 'keep'
from django.urls import path, include
urlpatterns = [
path('api/upload/', include('django_tus.urls')),
]
No frontend, bibliotecas como Uppy ajudam a orquestrar o envio em partes e a retomada, controlando tamanho de parte e tentativas automáticas. A integração define um endpoint TUS e um tamanho de chunk coerente com infraestrutura e latência. Mesmo com uma biblioteca, o ponto central é que o estado de quanto foi enviado fica registrado no servidor, permitindo retomar. A seguir está um exemplo em JavaScript que configura o Uppy com envio TUS.
// Exemplo ilustrativo: pressupõe que Uppy já esteja disponível no ambiente
const uppy = new Uppy.Core({
restrictions: {
maxFileSize: 10 * 1024 * 1024 * 1024
}
})
.use(Uppy.Dashboard, {
target: '#uppy-dashboard',
inline: true
})
.use(Uppy.Tus, {
endpoint: '/api/upload/',
chunkSize: 5 * 1024 * 1024,
retryDelays: [0, 1000, 3000, 5000]
});
uppy.on('complete', (resultado) => {
console.log('Uploads concluídos:', resultado.successful);
});
Ajustes essenciais no Django para suportar arquivos muito grandes
Além da estratégia de envio, o Django precisa estar ajustado para não tentar manter uploads grandes em memória. A configuração FILE_UPLOAD_HANDLERS direciona o Django a usar um handler que grava em arquivo temporário, reduzindo pressão sobre RAM. O parâmetro DATA_UPLOAD_MAX_MEMORY_SIZE controla limites de upload em memória e deve ser compatível com a estratégia escolhida. Também é importante definir um diretório temporário com espaço suficiente e monitorado.
Esses ajustes não substituem Nginx e servidor WSGI, mas evitam gargalos internos do framework. Quando o envio é em partes, cada parte ainda passa por Django, então handlers de arquivo e diretório temporário continuam relevantes. Em upload direto para nuvem, esses limites afetam mais os endpoints pequenos (como o de presigned URL), não o arquivo em si. A seguir está um conjunto de configurações típicas para uploads grandes.
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 * 1024 # 10GB
FILE_UPLOAD_HANDLERS = [
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]
FILE_UPLOAD_TEMP_DIR = '/tmp/django_uploads'
Configuração do Nginx para uploads grandes e streaming
Quando há um proxy reverso como Nginx na frente do Django, ele precisa aceitar corpos de requisição grandes e também permitir tempo suficiente para conexões lentas. O parâmetro client_max_body_size define o limite de tamanho do corpo, e timeouts do cliente evitam encerramentos prematuros. Para melhorar streaming e reduzir buffering em disco do proxy, proxy_request_buffering off costuma ser importante. Também é necessário aumentar timeouts de leitura e envio para requisições longas.
Mesmo com chunked upload, cada parte precisa caber no limite do Nginx, então o limite deve ser maior do que o chunk escolhido. Em upload direto para nuvem, o Nginx não participa do envio do arquivo, mas os endpoints do Django ainda passam por ele. Timeouts muito baixos causam falhas intermitentes difíceis de diagnosticar. A seguir está um exemplo de configuração comum para aceitar até 10GB e ajustar timeouts.
server {
listen 80;
server_name seuservidor.exemplo;
client_max_body_size 10G;
client_body_timeout 300s;
client_header_timeout 300s;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_request_buffering off;
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
}
}
Timeouts do servidor WSGI e impacto em uploads longos
O servidor WSGI, como Gunicorn, pode encerrar workers (processos de atendimento) se uma requisição demorar demais. Esse limite protege o sistema, mas pode interromper uploads grandes quando a conexão é lenta. Ajustar o timeout permite requisições mais longas, mas também aumenta o tempo que um worker fica ocupado. O efeito prático é que concorrência e escalabilidade dependem do número de workers e do padrão de tráfego.
Em chunked upload, as requisições são menores, reduzindo a chance de estourar timeouts. Em upload direto para nuvem, o Django não fica preso recebendo bytes por longos períodos, então o problema diminui drasticamente. Em TUS, as partes também ajudam, e a retomada reduz o prejuízo de uma queda. A seguir está um exemplo típico de ajuste de timeout no Gunicorn.
# Exemplo de configuração do Gunicorn (arquivo python de config)
timeout = 300 # 5 minutos
worker_class = 'sync'
workers = 4
Processamento em segundo plano com Celery após o upload
Arquivos grandes frequentemente precisam de etapas posteriores, como verificação de malware, extração de metadados, geração de miniaturas ou conversão de formato. Executar isso dentro da mesma requisição do upload torna o tempo de resposta enorme e aumenta chance de falhas. Uma abordagem comum usa processamento assíncrono, onde o servidor registra o upload como concluído e enfileira uma tarefa. O Celery é uma biblioteca popular para filas de tarefas em Python, integrada com Django.
O modelo de status ajuda a refletir o ciclo: uploading, processing e completed. O worker do Celery busca o registro no banco, altera o status e executa a lógica necessária. Em caso de exceção, o status pode ser marcado como failed, preservando rastreabilidade. A seguir está um exemplo de task que atualiza status e trata erro de forma controlada.
import logging
from celery import shared_task
from django.utils import timezone
from .models import UploadedFile
logger = logging.getLogger(__name__)
@shared_task
def processar_arquivo_enviado(file_id: int):
arquivo = None
try:
arquivo = UploadedFile.objects.get(id=file_id)
arquivo.status = 'processing'
arquivo.save(update_fields=['status'])
# Lógica de processamento do arquivo
# Exemplos comuns: varredura antivírus, conversão, extração de metadados
arquivo.status = 'completed'
arquivo.completed_at = timezone.now()
arquivo.save(update_fields=['status', 'completed_at'])
logger.info(f"Arquivo processado com sucesso: {file_id}")
except Exception as exc:
logger.error(f"Erro ao processar arquivo {file_id}: {str(exc)}")
if arquivo is not None:
arquivo.status = 'failed'
arquivo.save(update_fields=['status'])
Boas práticas de confiabilidade, segurança e manutenção
Uma implementação robusta depende de práticas que cobrem falhas inevitáveis em rede e infraestrutura. Repetição automática de partes falhadas reduz frustração e evita reinícios completos. A validação de tipo e tamanho deve existir tanto no frontend quanto no backend, pois controles no navegador não são garantia. Também é essencial limpar arquivos temporários órfãos para não consumir disco com uploads abandonados.
A lista a seguir resume práticas essenciais que costumam aparecer em sistemas maduros de upload grande. Ela reúne cuidados de arquitetura, segurança e operação contínua. A aplicação dessas medidas reduz incidentes e melhora previsibilidade do serviço. Os itens foram escolhidos por impacto direto em estabilidade e custo.
- Usar upload em partes para reduzir risco de timeout e facilitar retomada parcial.
- Exibir progresso baseado em partes concluídas para previsibilidade durante o envio.
- Implementar retentativas com backoff (intervalos crescentes) para falhas transitórias.
- Validar tipo e tamanho no backend para reduzir superfície de ataque.
- Preferir nuvem em produção para tirar carga de rede e disco do servidor Django.
- Processar em segundo plano para não prender requisições longas após o upload.
- Limpar temporários para evitar crescimento infinito de diretórios de chunk.
- Ajustar timeouts em Nginx e WSGI para evitar encerramentos prematuros.
- Monitorar espaço em disco e limites para não derrubar o ambiente por falta de armazenamento.
- Aplicar rate limiting para mitigar abuso por volume de uploads.
Conclusão
Uploads de até 10GB em Django exigem uma abordagem em camadas, pois o envio do arquivo envolve browser, proxy, servidor de aplicação, armazenamento e banco de dados. O upload em partes reduz falhas e simplifica progresso, enquanto o upload direto para nuvem melhora escalabilidade ao remover o tráfego pesado do servidor Django. O protocolo TUS adiciona retomada padronizada, sendo especialmente útil em conexões instáveis e em fluxos longos.
Uma solução completa também depende de configuração correta do Nginx e do servidor WSGI, além de registros de estado no banco e processamento assíncrono com Celery. Com esses elementos, o sistema evita consumo excessivo de memória, reduz timeouts e melhora a confiabilidade do envio. O resultado é um fluxo previsível e resiliente, capaz de sustentar arquivos muito grandes sem comprometer a estabilidade do servidor.