O Django carrega fama de “lento” há anos. Mesmo com todo o “batteries included”, o veredito que sempre aparece é o mesmo: “não aguenta escala”. Aí bate a dúvida clássica: vale a pena abandonar o Django e correr para um framework “rápido” em Node.js, Go ou Rust?
Mas tem um detalhe que não fecha com essa história. Se o Django é tão lento assim, por que gigantes como Instagram e Pinterest continuam rodando nele? Este artigo destrincha o que “lento” realmente significa aqui, de onde vem a diferença de performance entre Python e Go/Rust, e quando — se é que — vale trocar.
Antes de tudo: quatro conceitos
Quase toda confusão sobre performance some quando esses quatro conceitos ficam claros:
- Concorrência: um trabalhador alternando entre várias tarefas.
- Paralelismo: vários trabalhadores tocando várias tarefas ao mesmo tempo.
- I/O-bound: o trabalho está esperando algo externo (banco, rede). É como olhar a torradeira esperando a torrada pular.
- CPU-bound: o trabalho está sendo feito de verdade (cálculo, criptografia, serialização). É como moer o café na mão.
Em cima disso, dois modelos de execução. No sync, um trabalhador faz uma tarefa por vez, do início ao fim, sem desviar: o garçom anota o pedido, vai até a cozinha, espera ficar pronto, entrega — e só então atende o próximo cliente. No async, um trabalhador cuida de várias tarefas com um event loop coordenando: sempre que uma tarefa trava esperando I/O, o loop passa outra para o trabalhador, em vez de deixá-lo parado. O garçom anota o pedido, deixa na cozinha e já vai anotar o do próximo; quando a cozinha avisa que ficou pronto, ele volta para entregar.
O Django só é “lento” quando o tráfego explode
Com uma requisição comum (nem muito CPU, nem muito I/O), praticamente todo framework tem a mesma velocidade. O “lento” só aparece no volume. Vale um modelo simplificado para criar intuição: imagine 100 mil requisições, cada uma precisando de 1 segundo de I/O e 1 segundo de CPU, com um único worker.
- Modo WSGI (sync): o worker faz uma tarefa por vez — 2 segundos cada — e a fila leva ~200 mil segundos para escoar.
- Modo ASGI (async) — Django ou FastAPI, tanto faz: o async “esconde” o segundo de I/O (porque o worker pula para outra requisição enquanto espera), mas o segundo de CPU continua na fila. Dá ~100 mil segundos. Metade do tempo, na mesma máquina.
Esse número é uma ilustração de um worker para fixar a ideia — na vida real você roda vários processos e cores. Mas a lição vale: async ajuda no I/O, não no CPU. Na prática, o Django moderno deixa você escolher por view:
# Sync view (WSGI): o worker fica bloqueado durante a chamada ao banco
def profile(request):
user = User.objects.get(id=request.user.id) # blocks here
return JsonResponse({"name": user.name})
# Async view (ASGI): o worker serve outras requisições enquanto espera o I/O
async def profile(request):
user = await User.objects.aget(id=request.user.id) # frees the loop
return JsonResponse({"name": user.name})
Por que o Python em si é mais lento que Go ou Rust?
Quando se compara a execução pura do código (a parte de CPU), o Python perde para Go e Rust por três motivos bem concretos.
1. O GIL (Global Interpreter Lock). O CPython tem um cadeado com uma regra dura: apenas uma thread executa bytecode Python por vez. Isso torna multi-threading praticamente inútil para trabalho CPU-bound (para I/O-bound ainda ajuda). Resultado prático: rodar o mesmo código CPU-bound em 1 core ou em 8 cores dá quase no mesmo, porque 7 cores ficam ociosos. Go, por outro lado, já nasce com paralelismo real e um modelo de concorrência embutido (goroutines).
A saída no Python é multiprocessing: cada processo tem o seu próprio GIL, então vários processos rodam de verdade em paralelo.
# Threads não ajudam trabalho CPU-bound no CPython (GIL). Processos ajudam.
from concurrent.futures import ProcessPoolExecutor
def crunch(n):
return sum(i * i for i in range(n))
# Um GIL por processo => paralelismo real entre os cores
with ProcessPoolExecutor() as pool:
results = list(pool.map(crunch, [10_000_000] * 8))
2. O intermediário. O Python compila o seu código para bytecode e, em tempo de execução, o CPython interpreta esse bytecode instrução por instrução. Go e Rust compilam para código de máquina antes do deploy, então a CPU executa direto, sem intermediário.
3. Tipagem dinâmica. A cada a + b, o Python precisa perguntar em runtime: “o que é a? Um int? Uma string? Uma lista? Qual + usar?”. Go e Rust já conhecem os tipos em tempo de compilação e emitem uma única instrução de soma da CPU. Zero perguntas.
Como o Django escala mesmo assim
Como o gargalo de CPU vem do GIL, a forma clássica de escalar é rodar vários processos — um por core — atrás de um servidor de aplicação. É assim que se arranca paralelismo real de uma stack Python:
# WSGI: vários processos = paralelismo real (cada processo tem seu próprio GIL)
gunicorn myproject.wsgi:application --workers 8
# ASGI: worker async para concorrência de I/O, escalado em vários processos
gunicorn myproject.asgi:application -k uvicorn.workers.UvicornWorker --workers 8
O segredo não é um core mágico mais rápido, e sim multiplicar processos e máquinas, com o banco e o cache aguentando o tranco. É engenharia de infraestrutura, não de linguagem.
Então o Python está condenado na web?
De jeito nenhum. Sim, o Python é “mais lento” para servir requisição pura — mas isso atende com folga startups e empresas de porte médio. Tudo é trade-off: o Django é rápido de desenvolver, vem com tudo incluso, e a performance pode ser compensada com infraestrutura mais forte. Go e Rust são mais lentos de construir, mas servem mais rápido na mesma infra.
Veredito
Velocidade importa. Mas entregar importa mais. Não abandone o Django só por causa de benchmark. Cresça com ele até que um serviço específico realmente bata numa parede de performance — digamos, na casa dos 10 mil requests por segundo — e aí sim extraia aquele serviço e reescreva em algo mais eficiente, como Go, Rust ou Java. É otimização cirúrgica, no ponto que dói, não um rewrite por medo.
TL;DR
O Instagram não largou o Django — ele clonou o Django em milhares de servidores. O trabalho pesado de verdade aconteceu nos bancos de dados e caches, que foram fatiados (sharding), trocados e reconstruídos para suportar o volume gigante de requisições.
A sugestão que fica: domine um framework a fundo e entenda os demais por cima. Conhecer os prós e contras de cada um é o que permite tomar a decisão certa na hora certa — inclusive a decisão de não trocar de stack. Às vezes, a escolha de infraestrutura mais inteligente é a mais sem-graça: continuar no que você já entrega bem e escalar com cabeça. 🚀