Celery é uma fila de tarefas distribuída para executar trabalho fora do ciclo de uma requisição. Em vez de fazer o usuário esperar pelo envio de e-mail, geração de relatório ou processamento de imagem, a aplicação publica uma mensagem e um worker executa a função. A resposta direta é: use Celery quando o trabalho puder acontecer depois, precisar de retries ou tiver de ser distribuído entre processos e máquinas. Não o adote apenas para chamar uma função curta.
Celery coordena produtores e workers por meio de um broker; não é o broker nem o banco de dados. Neste exemplo, Redis será o broker que transporta mensagens. O resultado pode ser armazenado no próprio Redis, embora muitas aplicações não precisem guardar retornos. Antes de avançar, vale conhecer nosso guia de Redis com Python e as recomendações da documentação oficial do Celery.
Arquitetura e decisões práticas
Uma requisição HTTP valida os dados, persiste o estado necessário e chama a tarefa com .delay() ou .apply_async(). O broker recebe a mensagem; um worker disponível confirma e processa; opcionalmente, um backend registra estado e resultado. Essa separação melhora a latência da API, mas adiciona consistência eventual e operação distribuída. O endpoint deve retornar um identificador e o cliente deve consultar o estado, receber webhook ou acompanhar eventos.
Escolha Celery para tarefas com segundos ou minutos de duração, agendamento, filas separadas ou tolerância a falhas. Para código assíncrono dentro de um único processo, async/await em Python resolve outro problema: concorrência de I/O, não execução durável. Para trabalhos minúsculos sem requisito de entrega, o mecanismo de background do framework pode ser suficiente.
Configuração com Redis
Instale os pacotes e execute Redis localmente ou como serviço gerenciado. A documentação oficial do Redis explica persistência, autenticação e alta disponibilidade. Em produção, use TLS, credenciais, limites de memória e uma instância dimensionada para filas; não exponha Redis à internet.
pip install "celery[redis]"
celery -A tasks worker --loglevel=INFO
from celery import Celery
app = Celery(
"myapp",
broker="redis://localhost:6379/0",
backend="redis://localhost:6379/1",
)
app.conf.update(
task_serializer="json",
accept_content=["json"],
result_serializer="json",
timezone="UTC",
task_track_started=True,
task_time_limit=300,
task_soft_time_limit=270,
worker_prefetch_multiplier=1,
)
Mantenha URLs em variáveis de ambiente, nunca no repositório. JSON evita o risco de desserializar objetos Python arbitrários. Um backend de resultados tem custo de memória e manutenção; desative resultados com task_ignore_result quando ninguém os consultar. Para empacotar worker e broker de forma reproduzível, consulte Docker com Python.
Retries que não pioram a falha
Retry deve tratar erros transitórios, como timeout, indisponibilidade temporária ou resposta HTTP 429. Dados inválidos e regras de negócio rejeitadas são falhas permanentes. Repeti-las só congestiona a fila. Use backoff exponencial, jitter e limite de tentativas para evitar que milhares de tarefas ataquem novamente um serviço que está se recuperando.
import requests
@app.task(bind=True, max_retries=5)
def notify_customer(self, order_id: int) -> None:
try:
response = requests.post(
"https://api.example.com/notifications",
json={"order_id": order_id},
timeout=(3, 10),
)
except (requests.Timeout, requests.ConnectionError) as erro:
espera = min(2 ** self.request.retries, 600)
raise self.retry(exc=erro, countdown=espera)
if response.status_code in {429, 500, 502, 503, 504}:
retry_after = response.headers.get("Retry-After")
espera = int(retry_after) if retry_after and retry_after.isdigit() else min(2 ** self.request.retries, 600)
raise self.retry(exc=requests.HTTPError(response=response), countdown=espera)
response.raise_for_status() # Respostas 4xx permanentes falham sem retry.</code></pre>
Defina timeout também no cliente HTTP; o limite do Celery não substitui timeouts de rede. Não capture Exception para repetir tudo. Registre a causa e preserve a exceção. A referência de retries do Celery detalha o comportamento de retry e autoretry_for.
Idempotência: requisito, não otimização
Filas normalmente oferecem entrega pelo menos uma vez: uma mensagem pode ser processada novamente se o worker concluir o efeito externo e falhar antes da confirmação. Logo, a tarefa deve produzir o mesmo estado quando repetida. Cobrar um cartão, enviar um cupom ou incrementar um saldo sem proteção pode duplicar efeitos.
Use uma chave de idempotência estável, como charge:order:123, e imponha unicidade no banco. Grave estado e efeito na mesma transação quando possível. Para APIs externas, envie a chave no cabeçalho suportado pelo provedor. Um lock Redis pode reduzir concorrência, mas não substitui uma restrição única durável.
@app.task(bind=True, acks_late=True)
def issue_invoice(self, order_id: int) -> str:
invoice = Invoice.get_or_create_for_order(order_id)
if invoice.status == "issued":
return invoice.number
number = tax_api.issue(
order_id=order_id,
idempotency_key=f"invoice-order-{order_id}",
)
invoice.mark_issued(number)
return number</code></pre>
acks_late=True confirma após a execução e pode recuperar trabalho quando o processo morre, mas aumenta a chance de repetição. Só habilite em tarefas idempotentes. Passe IDs e valores simples, não objetos ORM serializados: o registro pode mudar entre publicação e consumo.
Filas, concorrência e prioridades
Separe tarefas por perfil: emails, reports e payments, por exemplo. Assim, um relatório pesado não bloqueia uma confirmação de pagamento. Direcione com task_routes e execute workers com concorrência adequada. Tarefas de CPU exigem processos e capacidade proporcional; tarefas de I/O aceitam maior concorrência, desde que o banco e os serviços externos suportem. Nosso comparativo de threads e processos em Python ajuda nessa decisão.
Prefetch alto favorece throughput em tarefas curtas, mas um worker pode reservar muitas mensagens longas e desequilibrar a fila. worker_prefetch_multiplier=1 é um início prudente para cargas demoradas. Prioridade no broker não é substituta para filas isoladas e capacidade reservada.
Observabilidade em produção
Monitore profundidade e idade da fila, duração, taxa de sucesso, retries, falhas definitivas e workers ativos. Logs devem ser estruturados e incluir task_id, nome da tarefa, tentativa e identificador de negócio. Não registre tokens nem dados pessoais. Integre métricas ao sistema de alertas e distribua um correlation ID da requisição para a tarefa.
Eventos do Celery e ferramentas como Flower ajudam na inspeção, mas não substituem métricas históricas e alertas. Use logging estruturado em Python, tracing distribuído e uma fila de mensagens mortas ou fluxo operacional para falhas esgotadas. Um alerta útil considera tempo: “a mensagem mais antiga supera o SLO”, não apenas “há mensagens”.
Erros comuns
- Publicar a tarefa antes de confirmar a transação do banco, permitindo que o worker procure um registro ainda invisível.
- Executar chamadas externas sem timeout ou repetir erros permanentes indefinidamente.
- Assumir entrega exatamente uma vez e ignorar idempotência.
- Colocar payloads grandes no broker em vez de armazená-los e enviar uma referência.
- Misturar tarefas rápidas e pesadas na mesma fila sem limites de tempo.
- Implantar código incompatível enquanto mensagens antigas ainda aguardam processamento.
Publique após o commit, ou use um padrão outbox quando perder a mensagem entre banco e broker for inaceitável. Versione payloads e faça deploy compatível durante a drenagem da fila. Teste a função de domínio sem Celery e mantenha testes de integração para publicação, retry e duplicidade; o guia de pytest em Python cobre a base dessa estratégia.
Perguntas frequentes
Celery substitui async/await?
Não. Celery distribui trabalho entre processos e pode persistir mensagens; async/await coordena operações concorrentes em um processo. Eles podem coexistir, mas resolvem problemas diferentes.
Redis é suficiente como broker em produção?
Pode ser, se persistência, disponibilidade, memória e segurança forem configuradas conforme o risco do sistema. Avalie RabbitMQ quando precisar de recursos avançados de mensageria e semânticas específicas.
Devo armazenar todos os resultados?
Não. Armazene somente quando a aplicação consultar o retorno. Para tarefas de efeito colateral, o estado de negócio no banco costuma ser a fonte de verdade.
Como testar retries sem esperar?
Teste a regra de negócio separadamente, simule exceções transitórias e inspecione a solicitação de retry. Complete com poucos testes usando broker e worker reais em ambiente isolado.
Conclusão
Uma implementação confiável de Celery não termina em .delay(). Ela classifica falhas, limita retries, torna efeitos idempotentes, separa filas, controla concorrência e mede atraso e duração. Comece com uma tarefa bem delimitada, Redis protegido, payload pequeno e um SLO observável. Só então amplie workers e rotas. Essa disciplina entrega o benefício real do background: reduzir a latência para o usuário sem esconder falhas operacionais.