Existe um momento quase inevitável para quem decide modernizar uma aplicação Django. Você lê sobre ASGI, vê benchmarks de frameworks assíncronos, troca def por async def em uma view e espera mais concorrência. Em vez disso, recebe:
django.core.exceptions.SynchronousOnlyOperation:
You cannot call this from an async context - use a thread or sync_to_async.
O código do banco não mudou. A query funcionava segundos antes. Foi apenas a assinatura da view que mudou — e agora o Django se recusa a executá-la.
Isso não é um bug nem uma limitação arbitrária. É uma proteção contra uma mistura perigosa de dois modelos de execução. O Django moderno consegue trabalhar no mundo síncrono e no assíncrono, mas a fronteira entre esses dois mundos ainda exige decisões explícitas. Entender essa fronteira é muito mais importante do que simplesmente adicionar await ao código.
Async não deixa uma operação individual mais rápida
O primeiro erro é acreditar que async significa velocidade. Se uma consulta ao PostgreSQL leva 50 milissegundos, ela continuará levando aproximadamente 50 milissegundos. O ganho está no que o processo faz durante essa espera.
No modelo síncrono, o worker fica ocupado aguardando a resposta. No modelo assíncrono, a coroutine devolve o controle ao event loop, que pode avançar outras requisições enquanto o banco, uma API externa ou a rede trabalha.
- I/O-bound: banco, HTTP, cache, arquivos e conexões longas podem se beneficiar de async.
- CPU-bound: compressão pesada, processamento de imagens, criptografia e cálculos não ficam mais rápidos com
asyncio.
Async é, portanto, uma ferramenta de concorrência. Ele melhora o aproveitamento do processo quando existem muitas esperas, mas não reduz magicamente o custo do trabalho.
O Django hoje opera em dois caminhos
O Django nasceu síncrono, sobre WSGI, e acumulou durante anos middlewares, backends, bibliotecas e APIs desenhadas para esse modelo. O suporte a ASGI e views assíncronas foi adicionado sem quebrar esse enorme ecossistema.
O resultado é uma aplicação capaz de percorrer dois caminhos:
- caminho síncrono: view, middleware e dependências tradicionais executam de forma bloqueante;
- caminho assíncrono: uma stack ASGI pode manter muitas operações de I/O em andamento no mesmo processo.
Quando um trecho async precisa chamar código sync, ou o contrário, o Django atravessa uma ponte. Essa adaptação é útil e segura, mas não é invisível: envolve threads, troca de contexto e regras para preservar conexões e estado local.
O modelo mental correto: event loop, coroutine e await
Para entender o comportamento do Django, imagine o event loop como um coordenador com uma única regra: nenhuma tarefa pode ocupar a pista por muito tempo. Cada função async def cria uma coroutine. Ela começa a executar e, quando chega a um await de uma operação realmente não bloqueante, devolve voluntariamente o controle ao loop.
async def load_page():
profile = await fetch_profile() # devolve o controle enquanto espera I/O
return render_profile(profile) # volta a executar quando o dado chega
O await não significa “rode em outra thread” e também não torna automaticamente a função chamada assíncrona. Ele só pausa corretamente quando o objeto aguardado coopera com o event loop. Se fetch_profile() usar internamente um socket bloqueante ou fizer um cálculo pesado durante dois segundos, o loop inteiro continua preso por dois segundos.
Essa distinção explica por que o código abaixo é assíncrono apenas na aparência:
import requests
async def fake_async_view(request):
response = requests.get("https://api.example.com/data") # bloqueia o loop
return JsonResponse(response.json())
Trocar requests por um cliente com I/O assíncrono, como httpx.AsyncClient, muda a natureza da espera. A assinatura da função é só a porta de entrada; o comportamento de todas as dependências define se o fluxo é realmente não bloqueante.
WSGI, ASGI e o ciclo completo de uma requisição
WSGI foi desenhado para o modelo clássico de uma chamada que recebe uma requisição e devolve uma resposta. Ele funciona muito bem para páginas tradicionais, mas não representa naturalmente eventos como mensagens de WebSocket, desconexões e respostas transmitidas durante muito tempo.
ASGI trabalha com um protocolo orientado a eventos. A aplicação recebe mensagens — início da requisição, partes do corpo, desconexão — e envia mensagens de resposta. Esse desenho permite HTTP assíncrono, streaming e outros protocolos no mesmo modelo.
Na prática, existem quatro combinações importantes:
- view sync em WSGI: caminho clássico, simples e previsível;
- view async em WSGI: o Django cria um loop por requisição; o código roda, mas sem o benefício de conexões assíncronas de longa duração;
- view sync em ASGI: o Django adapta o handler para uma thread; continua sendo uma configuração válida;
- view async em ASGI: caminho ideal para alta concorrência de I/O, desde que middleware e dependências também cooperem.
Essa tabela mental evita duas conclusões erradas: ASGI não obriga todas as views a serem async, e uma view async não exige que todo o projeto seja reescrito.
Por que surge o SynchronousOnlyOperation
Considere uma view tradicional:
from django.http import JsonResponse
from .models import Customer
def customer_detail(request, customer_id):
customer = Customer.objects.get(pk=customer_id)
return JsonResponse({"name": customer.name})
Agora altere apenas a assinatura:
async def customer_detail(request, customer_id):
customer = Customer.objects.get(pk=customer_id)
return JsonResponse({"name": customer.name})
objects.get() é a API síncrona do ORM. Executá-la na thread do event loop bloquearia todas as outras coroutines atendidas por aquele loop. Além disso, partes síncronas do Django dependem de estado associado à thread e não são seguras para acesso concorrente dessa forma.
Por isso o Django detecta que existe um event loop ativo e interrompe a operação com SynchronousOnlyOperation. A exceção não está atrapalhando o async; ela está impedindo que uma chamada bloqueante escondida destrua a concorrência ou provoque problemas de integridade.
A solução direta: usar a API assíncrona do ORM
Métodos de QuerySet que executam SQL possuem, em muitos casos, uma variante com prefixo a. O exemplo correto fica assim:
async def customer_detail(request, customer_id):
customer = await Customer.objects.aget(pk=customer_id)
return JsonResponse({"name": customer.name})
O padrão aparece em várias operações:
customer = await Customer.objects.acreate(name="Ana")
exists = await Customer.objects.filter(active=True).aexists()
updated = await Customer.objects.filter(active=False).aupdate(active=True)
deleted, details = await Customer.objects.filter(active=False).adelete()
Também existem métodos assíncronos em models e relações, como asave(), adelete(), aset() e aadd(). O detalhe importante é que nem todo método de QuerySet precisa de await.
QuerySet é preguiçoso: saiba onde a query realmente acontece
Operações como filter(), exclude(), order_by() e select_related() apenas constroem um QuerySet. Elas não acessam o banco naquele momento e, por isso, continuam com o mesmo nome:
queryset = (
Customer.objects
.filter(active=True)
.select_related("account")
.order_by("name")
)
customers = [customer async for customer in queryset]
A query ocorre quando o resultado é avaliado. Em código síncrono, isso pode acontecer com for, list(), len() ou acesso a um item. Dentro de uma função async, essas avaliações também podem bloquear.
# Errado: list() força avaliação síncrona
customers = list(Customer.objects.filter(active=True))
# Correto: iteração assíncrona
customers = [
customer
async for customer in Customer.objects.filter(active=True)
]
Esse é um dos pontos que mais confundem: construir o QuerySet é seguro; avaliá-lo pelo caminho síncrono não é.
Como saber quais métodos do ORM precisam de await
Uma regra simples resolve quase todos os casos: pergunte se o método apenas transforma a consulta ou se ele precisa conversar com o banco para devolver a resposta.
- Não executam SQL naquele ponto:
filter(),exclude(),annotate(),order_by(),select_related(),prefetch_related()evalues(). Eles continuam sem prefixoa. - Executam SQL e retornam um resultado:
get(),first(),exists(),count(),create(),update()edelete(). Em async, useaget(),afirst(),aexists(),acount(),acreate(),aupdate()eadelete(). - Avaliam por iteração: substitua
forporasync for.
Há ainda avaliações menos óbvias. Serializar um QuerySet, testar seu conteúdo em alguns contextos, aplicar list() ou acessar relações não carregadas pode disparar SQL. O ponto crítico não está apenas na linha em que o QuerySet nasceu, mas em qualquer lugar onde seus dados são exigidos.
Relacionamentos, lazy loading e o N+1 escondido
Mesmo depois de buscar um objeto com aget(), acessar uma relação que não foi carregada pode tentar executar outra query de forma implícita:
order = await Order.objects.aget(pk=order_id)
customer_name = order.customer.name # pode tentar lazy loading síncrono
Carregue relações previsíveis na query principal:
order = await (
Order.objects
.select_related("customer")
.aget(pk=order_id)
)
customer_name = order.customer.name # já está em memória
Para coleções, use prefetch_related() antes da avaliação assíncrona. Isso não é apenas uma exigência do async: elimina o clássico problema N+1, reduz viagens ao banco e torna o custo da rota mais previsível.
Async for não significa uma query por item
async for permite que a avaliação do QuerySet aconteça sem bloquear o event loop pelo caminho síncrono. Ele não transforma cada linha em uma consulta separada e não promete que o banco entregará todos os registros em paralelo. O driver, a conexão e o QuerySet continuam determinando como os resultados são buscados e armazenados.
Também não existe vantagem automática em disparar várias queries independentes com asyncio.gather(). Uma única conexão costuma serializar operações, enquanto várias conexões aumentam pressão sobre o pool e o PostgreSQL. Antes de paralelizar queries, procure uma consulta melhor, select_related(), prefetch_related(), agregação ou cache.
Quando usar sync_to_async
Nem toda regra de negócio possui uma API assíncrona equivalente. Código legado, pacotes de terceiros e, principalmente, fluxos transacionais podem continuar síncronos. Nesses casos, sync_to_async() cria uma ponte controlada:
from asgiref.sync import sync_to_async
from django.db import transaction
@sync_to_async
def confirm_order(order_id):
with transaction.atomic():
order = Order.objects.select_for_update().get(pk=order_id)
order.status = Order.Status.CONFIRMED
order.save(update_fields=["status"])
Payment.objects.create(order=order)
return order.pk
async def confirm_order_view(request, order_id):
confirmed_id = await confirm_order(order_id)
return JsonResponse({"id": confirmed_id, "status": "confirmed"})
A recomendação prática é atravessar a fronteira uma vez por unidade de trabalho. Embrulhar cada query separadamente espalha adaptações pelo fluxo, dificulta o raciocínio sobre transações e aumenta o número de trocas entre os dois contextos.
O que sync_to_async realmente faz
sync_to_async() não converte código bloqueante em I/O assíncrono nativo. Ele desloca a função síncrona para uma thread apropriada e devolve ao event loop um objeto aguardável. Enquanto essa thread trabalha, o loop pode continuar atendendo outras coroutines.
Por padrão, o adaptador usa thread_sensitive=True. Esse modo existe porque drivers de banco e muito código histórico do Django assumem afinidade com a thread: o recurso deve ser usado no mesmo contexto em que foi criado. Valores de threadlocals e contextvars são preservados durante a travessia.
from asgiref.sync import sync_to_async
# Forma direta
result = await sync_to_async(load_legacy_report, thread_sensitive=True)(user.id)
# Como decorator
@sync_to_async(thread_sensitive=True)
def load_legacy_report(user_id):
return list(Report.objects.filter(user_id=user_id))
thread_sensitive=False não deve ser usado como uma “otimização” genérica para ORM. Ele é apropriado apenas quando a função é comprovadamente independente de estado sensível a thread. Desabilitar essa proteção sem entender a biblioteca chamada pode produzir erros intermitentes muito difíceis de reproduzir.
E o caminho inverso: async_to_sync
Às vezes um fluxo síncrono precisa chamar uma função async — por exemplo, uma integração moderna dentro de um comando legado. O adaptador async_to_sync() faz o caminho inverso e preserva contexto melhor do que simplesmente espalhar asyncio.run():
from asgiref.sync import async_to_sync
async def notify_partner(order_id):
...
def legacy_command():
async_to_sync(notify_partner)(order_id=42)
Essas pontes são ferramentas de interoperabilidade, não um estilo arquitetural a ser repetido a cada linha. Quanto mais vezes o fluxo alterna entre sync e async, mais difícil fica entender afinidade de thread, exceções, cancelamento e consumo de recursos.
Transações continuam sendo a grande fronteira
O ORM oferece muitas operações assíncronas, mas transações ainda não funcionam como um bloco totalmente async. Não presuma que este código é suportado:
# Não use como se transaction.atomic fosse async nativo
async with transaction.atomic():
order = await Order.objects.aget(pk=order_id)
await order.asave()
Quando o fluxo exige transaction.atomic(), select_for_update() ou várias mudanças que precisam ser confirmadas juntas, mantenha a unidade transacional em uma função síncrona e chame essa função com sync_to_async(). Isso preserva as garantias do banco e torna a fronteira arquitetural explícita.
Não passe conexões do banco entre threads
Objetos de conexão e cursores são sensíveis à thread em que foram criados. Evite capturar django.db.connection fora da função adaptada e passá-lo para outra thread.
# Evite transportar connection ou cursor pela fronteira
cursor = connection.cursor()
result = await sync_to_async(cursor.execute)("SELECT 1")
# Encapsule todo o acesso dentro da função síncrona
@sync_to_async
def run_report():
with connection.cursor() as cursor:
cursor.execute("SELECT COUNT(*) FROM orders")
return cursor.fetchone()[0]
O mesmo princípio vale para qualquer objeto cuja segurança dependa de thread local: leve dados simples pela fronteira, não recursos abertos.
ASGI é necessário para colher o benefício completo
Uma view async def também roda sob WSGI, mas o Django precisa criar um event loop específico para aquela requisição. Isso permite usar APIs assíncronas, porém não entrega a principal vantagem operacional: manter muitas conexões longas com uma stack assíncrona contínua.
Para aproveitar async de ponta a ponta, faça deploy com ASGI, usando servidores compatíveis como Uvicorn, Daphne ou Hypercorn:
uvicorn config.asgi:application --host 0.0.0.0 --port 8000
Mas ASGI sozinho não resolve tudo. Uma view async cercada por middleware exclusivamente síncrono obriga o Django a adaptar o fluxo. Dependências bloqueantes dentro da view — um cliente HTTP síncrono, por exemplo — continuam bloqueantes.
A cadeia inteira precisa ser observada
Antes de chamar uma rota de “assíncrona”, revise todo o caminho:
- servidor ASGI;
- middlewares compatíveis com async;
- autenticação e sessões usadas pelo caminho assíncrono;
- queries com APIs assíncronas ou fronteiras bem encapsuladas;
- clientes HTTP, cache e SDKs não bloqueantes;
- ausência de trabalho pesado de CPU dentro do event loop.
O Django pode registrar adaptações de middleware no logger django.request. Essa informação, combinada com tracing e testes de carga, ajuda a encontrar trechos que fazem a aplicação alternar entre sync e async sem necessidade.
Middleware híbrido: uma única classe pode atender os dois modos
Um middleware exclusivamente síncrono no meio de uma cadeia ASGI assíncrona força adaptação. Pacotes de terceiros antigos são candidatos frequentes. Ao escrever middleware próprio, declare corretamente a capacidade de operar nos dois contextos ou mantenha implementações separadas e explícitas.
Mais importante do que marcar flags é garantir que o trabalho interno respeite o modo escolhido. Um middleware “compatível com async” que grava logs por uma biblioteca bloqueante ou consulta o banco pela API síncrona ainda pode prejudicar a concorrência.
LOGGING = {
"version": 1,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {
"django.request": {
"handlers": ["console"],
"level": "DEBUG",
}
},
}
Em desenvolvimento ou homologação, procure a mensagem Asynchronous handler adapted for middleware .... Ela aponta onde o Django precisou mudar de modelo.
Autenticação, sessão, cache e signals
O suporte assíncrono do Django vai além das views e do ORM. Versões modernas oferecem APIs assíncronas em autenticação, sessão, cache e despacho de signals. Ainda assim, o backend concreto e pacotes de terceiros podem usar adaptações internas.
# Autenticação
user = await request.auser()
# Cache
payload = await cache.aget(cache_key)
await cache.aset(cache_key, payload, timeout=60)
# Signal customizado
await order_confirmed.asend(sender=Order, order=order)
Signals aceitam receivers síncronos e assíncronos e agrupam chamadas conforme o tipo, adaptando quando necessário. Isso é conveniente, mas não transforma signals em uma fila de tarefas: o emissor ainda espera o processamento. Para e-mail, webhooks, processamento pesado e rotinas que precisam de retry, uma fila continua sendo a ferramenta correta.
Conexões persistentes e pooling exigem atenção
Em modo assíncrono, a documentação do Django recomenda desabilitar conexões persistentes configuradas por CONN_MAX_AGE e usar o pooling oferecido pelo backend ou por uma solução externa adequada.
Mais concorrência na aplicação também significa potencialmente mais queries simultâneas. Se o pool comporta 20 conexões e a aplicação dispara 200 operações concorrentes, o gargalo apenas muda de lugar. O event loop não remove limites do PostgreSQL, do pool ou de APIs externas.
Concorrência precisa de limites
asyncio.gather() é poderoso, mas disparar centenas de chamadas ao mesmo tempo pode derrubar o serviço parceiro, esgotar sockets ou ocupar todo o pool. Concorrência saudável é concorrência limitada.
import asyncio
semaphore = asyncio.Semaphore(10)
async def fetch_limited(client, url):
async with semaphore:
async with asyncio.timeout(3):
response = await client.get(url)
response.raise_for_status()
return response.json()
Defina limites de conexão no cliente HTTP, timeouts por operação e uma política clara para falhas parciais. Sem isso, o ganho de throughput em condições normais vira uma avalanche durante a degradação de uma dependência.
Cancelamento e desconexão do cliente
Em respostas longas, o navegador pode fechar a conexão antes do término. Sob ASGI, o Django sinaliza esse cenário com asyncio.CancelledError. Use-o para liberar recursos e cancelar trabalho local, mas sempre propague a exceção:
import asyncio
async def stream_report(request):
try:
return await build_streaming_response()
except asyncio.CancelledError:
await release_temporary_resources()
raise
Capturar Exception indiscriminadamente, ignorar cancelamento e continuar processando pode desperdiçar CPU, conexões e chamadas pagas depois que já não existe cliente esperando a resposta.
Um caso em que async realmente brilha
Imagine um dashboard que consulta três serviços independentes. No modelo síncrono, as latências se somam. No assíncrono, as chamadas podem acontecer ao mesmo tempo:
import asyncio
import httpx
from django.http import JsonResponse
async def dashboard(request):
async with httpx.AsyncClient(timeout=3.0) as client:
profile, invoices, alerts = await asyncio.gather(
client.get("https://profiles.internal/me"),
client.get("https://billing.internal/invoices"),
client.get("https://alerts.internal/current"),
)
return JsonResponse({
"profile": profile.json(),
"invoices": invoices.json(),
"alerts": alerts.json(),
})
Se cada serviço demora aproximadamente 200 ms, a espera total pode ficar perto da chamada mais lenta, em vez de se aproximar da soma das três. Esse é o tipo de cenário em que a complexidade adicional costuma se pagar.
Tratando falhas no fan-out
No exemplo anterior, uma única exceção pode interromper o conjunto. Em produção, decida explicitamente quais dependências são obrigatórias e quais podem falhar sem derrubar o dashboard.
results = await asyncio.gather(
fetch_profile(client),
fetch_invoices(client),
fetch_alerts(client),
return_exceptions=True,
)
profile, invoices, alerts = results
if isinstance(profile, Exception):
raise ServiceUnavailable("profile service unavailable")
if isinstance(alerts, Exception):
alerts = [] # degradação aceitável e observada
return_exceptions=True não é uma receita universal; ele apenas entrega as falhas como valores. O sistema ainda precisa registrar, medir e classificar cada erro. Sem uma política de degradação, o código assíncrono só faz várias coisas falharem mais rapidamente.
Background task não é tarefa solta no event loop
Evite responder ao usuário e deixar trabalho importante em asyncio.create_task(). O processo pode reiniciar, a tarefa pode ser cancelada e você não terá persistência, retry ou rastreabilidade.
Use o event loop para concorrência que pertence ao ciclo da requisição. Para trabalhos que precisam sobreviver à resposta — envio de e-mail, geração de relatório, cobrança, processamento de mídia — use uma fila ou o mecanismo de tarefas adotado pela aplicação, com idempotência, retry e monitoramento.
Um caso em que async só adiciona complexidade
Agora pense em uma página CRUD que faz uma query rápida, renderiza um template e termina. Transformá-la em async pode exigir adaptação de middleware, revisão de bibliotecas e novas regras de teste sem produzir ganho mensurável.
Views síncronas continuam sendo uma excelente escolha quando:
- o fluxo é curto e dominado por uma ou duas queries;
- há dependência forte de código ou pacotes síncronos;
- transações ocupam a maior parte da regra de negócio;
- não existem conexões longas nem fan-out para serviços externos;
- testes de carga não demonstram benefício real.
Não existe prêmio por tornar 100% do projeto assíncrono. Uma aplicação híbrida, com fronteiras claras, pode ser mais simples e mais eficiente.
Erros comuns ao adotar async no Django
- Trocar todas as views de uma vez: migre apenas rotas cujo perfil de I/O justifica a mudança.
- Usar requests dentro de async def: prefira um cliente assíncrono, como
httpx.AsyncClient, ou isole a chamada síncrona. - Forçar QuerySet com list(): use
async forou uma compreensão assíncrona. - Espalhar sync_to_async em cada linha: encapsule uma unidade síncrona coerente.
- Ativar DJANGO_ALLOW_ASYNC_UNSAFE: essa variável desliga a proteção e pode causar corrupção de dados sob concorrência; não é solução para produção.
- Confundir concorrência com paralelismo: tarefas CPU-bound devem ir para processos, workers ou filas, não para o event loop.
- Ignorar o pool de conexões: o banco precisa acompanhar a concorrência planejada.
Como testar views e fluxos assíncronos
O cliente de testes tradicional consegue chamar views async. Quando o próprio teste é assíncrono, use o AsyncClient disponibilizado pelo Django:
from django.test import TestCase
class DashboardTests(TestCase):
async def test_dashboard_returns_profile(self):
response = await self.async_client.get("/dashboard/")
self.assertEqual(response.status_code, 200)
self.assertIn("profile", response.json())
Teste também comportamento, não apenas status HTTP:
- timeout de uma dependência;
- falha parcial em chamadas concorrentes;
- cancelamento e limpeza de recursos;
- limite de concorrência;
- ausência de chamadas síncronas acidentais no caminho async;
- quantidade de queries e ausência de N+1.
Mocks assíncronos devem reproduzir coroutines de verdade. Um mock que retorna instantaneamente pode esconder problemas de ordem, timeout e cancelamento; por isso, inclua ao menos alguns testes de integração com clientes e banco reais.
Como medir se a migração valeu a pena
Comparar apenas a média de latência com uma única requisição quase nunca revela o valor do async. O teste precisa aumentar a concorrência e reproduzir o perfil real de espera.
Registre pelo menos:
- throughput: requisições concluídas por segundo;
- latência p50, p95 e p99: principalmente a cauda sob carga;
- erros e timeouts: um número alto de respostas não significa sucesso;
- threads e event-loop lag: mostram bloqueio e adaptações excessivas;
- conexões de banco e espera no pool: revelam gargalos deslocados;
- CPU e memória por worker: ajudam a dimensionar o deploy.
Faça três comparações: implementação sync em WSGI, implementação sync em ASGI e implementação async em ASGI. Use o mesmo banco, limites, dados e padrão de carga. O objetivo não é provar que async vence; é descobrir qual arquitetura entrega o melhor comportamento com complexidade aceitável.
Observabilidade: enxergando bloqueios que não geram exceção
O Django protege suas próprias áreas async-unsafe, mas não consegue impedir que toda biblioteca externa bloqueie o loop. Um SDK síncrono pode funcionar sem erro e ainda aumentar brutalmente a latência p99.
Combine tracing distribuído, métricas do pool, logs estruturados e monitoramento do atraso do event loop. Em cada span, diferencie tempo de CPU, espera por conexão, execução SQL e chamadas externas. Quando a rota degrada sob concorrência, essa decomposição mostra se o problema é uma ponte sync/async, um pool pequeno, uma dependência lenta ou trabalho CPU-bound.
Arquitetura em camadas: uma forma sustentável de misturar sync e async
Um projeto híbrido fica mais fácil de manter quando a regra de negócio não depende diretamente do tipo da view. Uma organização prática separa:
- views: coordenam HTTP, autenticação e serialização;
- serviços assíncronos: fazem fan-out de rede e compõem resultados;
- serviços transacionais síncronos: concentram invariantes e mudanças atômicas no banco;
- workers: executam trabalho durável fora da requisição.
# services/orders.py — unidade transacional síncrona
@transaction.atomic
def reserve_order(order_id, user_id):
order = Order.objects.select_for_update().get(pk=order_id)
order.reserve_for(user_id)
order.save()
return order.pk
# views.py — orquestração assíncrona
async def reserve(request, order_id):
user = await request.auser()
order_pk = await sync_to_async(reserve_order)(order_id, user.pk)
shipping = await shipping_client.quote(order_pk)
return JsonResponse({"order": order_pk, "shipping": shipping})
A fronteira aparece uma vez, com nome e responsabilidade claros. Isso é muito mais seguro do que converter models, managers e helpers aleatoriamente até a exceção desaparecer.
Decisão prática: sync, async ou híbrido?
- CRUD, admin, backoffice e regra transacional: comece síncrono.
- API que agrega vários serviços externos: async costuma trazer ganho real.
- streaming, long polling e SSE: ASGI e async são o caminho natural.
- WebSockets: use uma stack ASGI apropriada e pense em estado, backpressure e distribuição.
- processamento de CPU: use processos ou workers; async não resolve.
- fluxo com I/O externo e transação local: arquitetura híbrida, com cada parte no modelo que a atende melhor.
Estratégia segura para migrar uma aplicação existente
- Meça primeiro: identifique rotas com espera de rede, conexões longas ou alto volume concorrente.
- Adote ASGI: valide o comportamento da aplicação antes de converter views.
- Audite middleware e bibliotecas: descubra onde haverá adaptação ou bloqueio.
- Migre uma rota: troque clientes externos e operações do ORM pelas APIs async correspondentes.
- Isole blocos transacionais: mantenha-os síncronos atrás de uma única ponte.
- Teste sob carga: compare latência p95/p99, throughput, threads, conexões e consumo de memória.
- Mantenha o que não melhorou em sync: complexidade sem ganho é regressão arquitetural.
Checklist rápido
[ ] A rota espera várias operações independentes de I/O?
[ ] A aplicação está rodando em ASGI?
[ ] O middleware do caminho suporta async?
[ ] Toda biblioteca de rede usada na view é não bloqueante?
[ ] Queries avaliadas usam aget(), acreate(), async for etc.?
[ ] Transações estão encapsuladas em uma função sync?
[ ] CONN_MAX_AGE e pooling foram revisados?
[ ] O ganho apareceu em um teste de carga real?
Conclusão
O problema do async no Django não é que o framework “falhou” em se tornar assíncrono. O desafio é preservar compatibilidade com um ecossistema síncrono maduro enquanto novas APIs são adicionadas de forma segura.
A regra mais útil é simples: use async onde existe espera concorrente suficiente para justificar async. Use as variantes assíncronas do ORM quando disponíveis, mantenha transações como unidades síncronas bem encapsuladas e trate cada travessia entre os dois mundos como uma decisão arquitetural.
Trocar def por async def é apenas a primeira linha. A aplicação só se torna realmente assíncrona quando servidor, middleware, bibliotecas, banco e desenho do fluxo trabalham na mesma direção. Até lá, o Django continuará sendo dois modelos sob o mesmo teto — e isso pode ser uma vantagem, desde que você saiba exatamente em qual deles está pisando.