Algumas operações são pesadas demais para acontecer durante uma requisição HTTP — enviar e-mails em massa, gerar relatórios PDF, processar imagens, treinar modelos de machine learning, sincronizar dados com APIs externas. Fazer o usuário esperar por tudo isso é inaceitável. Celery resolve esse problema: ele executa tarefas em segundo plano, de forma distribuída, com suporte a agendamento, retentativas automáticas e monitoramento. É o padrão da indústria para processamento assíncrono em Python.
Arquitetura do Celery
┌─────────────┐ enfileira ┌─────────────┐ consome ┌─────────────┐
│ Aplicação │ ────────────────► │ Broker │ ──────────────► │ Worker │
│ (FastAPI/ │ │ (Redis/ │ │ (Celery) │
│ Flask) │ │ RabbitMQ) │ │ │
└─────────────┘ └─────────────┘ └──────┬──────┘
▲ │
│ ┌─────────────┐ │ armazena
│ lê resultado │ Backend │ ◄──────────────────────┘ resultado
└──────────────────────── │ (Redis) │
└─────────────┘
- Producer — a aplicação que enfileira tarefas
- Broker — fila de mensagens (Redis ou RabbitMQ)
- Worker — processo que consome e executa as tarefas
- Backend — armazena resultados das tarefas (Redis, banco de dados)
Instalação
pip install celery redis
pip install flower # monitoramento web
# Redis via Docker
docker run -d --name redis -p 6379:6379 redis:7-alpine
Configuração Básica
# celery_app.py
from celery import Celery
def criar_celery(broker_url="redis://localhost:6379/0",
backend_url="redis://localhost:6379/1"):
app = Celery(
"escola",
broker=broker_url,
backend=backend_url,
include=["tarefas.email", "tarefas.relatorio", "tarefas.dados"]
)
app.conf.update(
# Serialização
task_serializer = "json",
result_serializer = "json",
accept_content = ["json"],
# Timezone
timezone = "America/Sao_Paulo",
enable_utc = True,
# Comportamento das tarefas
task_track_started = True,
task_acks_late = True, # confirma após execução
worker_prefetch_multiplier = 1, # uma tarefa por vez por worker
# Retentativas
task_max_retries = 3,
task_default_retry_delay = 60, # segundos
# Resultados
result_expires = 3600, # 1 hora
# Filas
task_default_queue = "padrao",
task_queues = {
"padrao": {"exchange": "padrao"},
"prioritaria": {"exchange": "prioritaria"},
"pesada": {"exchange": "pesada"},
}
)
return app
celery = criar_celery()
Definindo Tarefas
# tarefas/email.py
from celery_app import celery
from celery.utils.log import get_task_logger
import time
logger = get_task_logger(__name__)
@celery.task(
name="email.enviar_boas_vindas",
bind=True, # acesso a self (instância da tarefa)
max_retries=3,
default_retry_delay=30,
queue="prioritaria"
)
def enviar_boas_vindas(self, email: str, nome: str):
"""Envia e-mail de boas-vindas para novo usuário."""
logger.info(f"Enviando boas-vindas para {email}")
try:
# Simulando envio de e-mail
time.sleep(2)
if "@invalido" in email:
raise ValueError(f"Domínio inválido: {email}")
logger.info(f"E-mail enviado com sucesso para {email}")
return {"status": "enviado", "destinatario": email}
except Exception as exc:
logger.error(f"Falha ao enviar para {email}: {exc}")
raise self.retry(exc=exc, countdown=30 * (self.request.retries + 1))
@celery.task(name="email.enviar_em_massa", queue="pesada")
def enviar_em_massa(destinatarios: list, assunto: str, corpo: str):
"""Envia e-mail para múltiplos destinatários."""
enviados = 0
falhas = []
for dest in destinatarios:
try:
time.sleep(0.1) # simulando envio
enviados += 1
logger.info(f"Enviado para {dest}")
except Exception as e:
falhas.append({"email": dest, "erro": str(e)})
return {
"total": len(destinatarios),
"enviados": enviados,
"falhas": len(falhas),
"detalhes": falhas
}
# tarefas/relatorio.py
from celery_app import celery
from celery.utils.log import get_task_logger
from celery import current_task
import time
logger = get_task_logger(__name__)
@celery.task(
name="relatorio.gerar_pdf",
bind=True,
queue="pesada",
time_limit=300 # mata a tarefa após 5 minutos
)
def gerar_relatorio_pdf(self, aluno_ids: list, periodo: str):
"""Gera relatório PDF de alunos — tarefa longa."""
total = len(aluno_ids)
logger.info(f"Gerando relatório para {total} alunos")
for i, aluno_id in enumerate(aluno_ids, 1):
# Atualiza progresso
self.update_state(
state="PROGRESS",
meta={
"atual": i,
"total": total,
"percentual": round(i / total * 100, 1),
"aluno_id": aluno_id
}
)
time.sleep(0.5) # simula processamento
caminho = f"/relatorios/relatorio_{periodo}.pdf"
logger.info(f"Relatório gerado: {caminho}")
return {
"status": "concluido",
"caminho": caminho,
"total": total,
"periodo": periodo
}
Integrando com FastAPI
# main.py
from fastapi import FastAPI, BackgroundTasks, HTTPException
from pydantic import BaseModel, EmailStr
from celery.result import AsyncResult
from celery_app import celery
from tarefas.email import enviar_boas_vindas, enviar_em_massa
from tarefas.relatorio import gerar_relatorio_pdf
from typing import List
app = FastAPI(title="API com Celery")
class NovoUsuario(BaseModel):
nome: str
email: EmailStr
class EnvioMassa(BaseModel):
destinatarios: List[EmailStr]
assunto: str
corpo: str
class SolicitacaoRelatorio(BaseModel):
aluno_ids: List[int]
periodo: str
# Enfileirar tarefa e retornar ID
@app.post("/usuarios/registro", status_code=201)
async def registrar_usuario(dados: NovoUsuario):
# Tarefa assíncrona — não bloqueia a resposta
tarefa = enviar_boas_vindas.delay(dados.email, dados.nome)
return {
"mensagem": "Usuário registrado. E-mail de boas-vindas será enviado.",
"tarefa_id": tarefa.id
}
@app.post("/emails/massa")
async def envio_em_massa(dados: EnvioMassa):
tarefa = enviar_em_massa.apply_async(
args=[dados.destinatarios, dados.assunto, dados.corpo],
queue="pesada",
countdown=5 # aguarda 5 segundos antes de iniciar
)
return {
"mensagem": f"Envio em massa para {len(dados.destinatarios)} destinatários enfileirado.",
"tarefa_id": tarefa.id,
"total": len(dados.destinatarios)
}
@app.post("/relatorios/gerar")
async def solicitar_relatorio(dados: SolicitacaoRelatorio):
tarefa = gerar_relatorio_pdf.apply_async(
args=[dados.aluno_ids, dados.periodo],
queue="pesada"
)
return {
"mensagem": "Geração de relatório iniciada.",
"tarefa_id": tarefa.id
}
# Verificar status de qualquer tarefa
@app.get("/tarefas/{tarefa_id}")
async def verificar_tarefa(tarefa_id: str):
resultado = AsyncResult(tarefa_id, app=celery)
resposta = {
"id": tarefa_id,
"status": resultado.status,
}
if resultado.status == "PROGRESS":
resposta["progresso"] = resultado.info
elif resultado.status == "SUCCESS":
resposta["resultado"] = resultado.get()
elif resultado.status == "FAILURE":
resposta["erro"] = str(resultado.result)
return resposta
# Revogar (cancelar) uma tarefa
@app.delete("/tarefas/{tarefa_id}")
async def cancelar_tarefa(tarefa_id: str):
celery.control.revoke(tarefa_id, terminate=True)
return {"mensagem": f"Tarefa {tarefa_id} cancelada."}
Agendamento de Tarefas: Celery Beat
# celery_app.py — adicionando agendamento
from celery.schedules import crontab
app.conf.beat_schedule = {
# A cada hora
"backup-banco-horario": {
"task": "dados.backup_banco",
"schedule": 3600,
"args": ("escola",)
},
# Todo dia às 6h
"relatorio-diario": {
"task": "relatorio.gerar_diario",
"schedule": crontab(hour=6, minute=0),
},
# Segunda a sexta às 8h30
"resumo-semanal": {
"task": "email.enviar_resumo_semanal",
"schedule": crontab(hour=8, minute=30, day_of_week="1-5"),
},
# Primeiro dia do mês
"relatorio-mensal": {
"task": "relatorio.gerar_mensal",
"schedule": crontab(day_of_month=1, hour=7, minute=0),
},
# A cada 30 minutos
"sincronizar-dados": {
"task": "dados.sincronizar",
"schedule": crontab(minute="*/30"),
},
}
# tarefas/agendadas.py
from celery_app import celery
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
@celery.task(name="dados.backup_banco")
def backup_banco(nome_banco: str):
logger.info(f"Iniciando backup de {nome_banco}")
# lógica de backup...
return {"status": "ok", "banco": nome_banco}
@celery.task(name="relatorio.gerar_diario")
def gerar_relatorio_diario():
from datetime import date
hoje = date.today().isoformat()
logger.info(f"Gerando relatório diário: {hoje}")
# lógica do relatório...
return {"data": hoje, "status": "gerado"}
@celery.task(name="email.enviar_resumo_semanal")
def enviar_resumo_semanal():
logger.info("Enviando resumo semanal para gestores")
# busca dados, monta e-mail, envia...
return {"destinatarios": 15, "status": "enviado"}
Chains, Groups e Chords: Workflows Complexos
from celery import chain, group, chord
from tarefas.dados import buscar_dados, processar_chunk, consolidar
# CHAIN — tarefas em sequência (saída de uma é entrada da próxima)
pipeline = chain(
buscar_dados.s(periodo="2024-03"),
processar_chunk.s(),
consolidar.s()
)
resultado = pipeline.delay()
# GROUP — tarefas em paralelo
processamentos = group(
processar_chunk.s(chunk) for chunk in range(1, 6)
)
resultados = processamentos.delay()
# CHORD — paralelo com callback ao final
workflow = chord(
group(processar_chunk.s(i) for i in range(1, 6)),
consolidar.s() # chamado quando todos terminam
)
workflow.delay()
# Exemplo prático — pipeline de importação
from tarefas.importacao import (
validar_csv,
transformar_dados,
inserir_banco,
notificar_conclusao
)
def importar_arquivo(caminho_csv: str, usuario_email: str):
pipeline = chain(
validar_csv.s(caminho_csv),
transformar_dados.s(),
inserir_banco.s(),
notificar_conclusao.s(usuario_email)
)
return pipeline.apply_async()
Monitoramento com Flower
# Iniciando o Flower
celery -A celery_app flower --port=5555
# Acesse: http://localhost:5555
# docker-compose.yml — adicionando Celery e Flower
services:
worker-padrao:
build: .
command: celery -A celery_app worker -Q padrao -c 4 --loglevel=info
environment:
- REDIS_URL=redis://redis:6379/0
depends_on:
- redis
worker-pesado:
build: .
command: celery -A celery_app worker -Q pesada -c 2 --loglevel=info
environment:
- REDIS_URL=redis://redis:6379/0
depends_on:
- redis
worker-prioritario:
build: .
command: celery -A celery_app worker -Q prioritaria -c 8 --loglevel=info
depends_on:
- redis
beat:
build: .
command: celery -A celery_app beat --loglevel=info
depends_on:
- redis
flower:
build: .
command: celery -A celery_app flower --port=5555
ports:
- "5555:5555"
depends_on:
- redis
Boas Práticas
# 1. Tarefas idempotentes — podem ser executadas múltiplas vezes sem efeito colateral
@celery.task(name="email.confirmar_cadastro")
def confirmar_cadastro(usuario_id: int):
from models import Usuario
usuario = Usuario.buscar(usuario_id)
if usuario.email_confirmado:
return {"status": "ignorado", "motivo": "já confirmado"}
# só processa se ainda não confirmado
usuario.confirmar_email()
return {"status": "confirmado"}
# 2. Sempre defina timeout
@celery.task(name="dados.processar", time_limit=120, soft_time_limit=100)
def processar_dados(dados_id: int):
from celery.exceptions import SoftTimeLimitExceeded
try:
# processamento...
pass
except SoftTimeLimitExceeded:
# cleanup antes do hard limit
logger.warning(f"Soft limit atingido para dados {dados_id}")
raise
# 3. Não passe objetos complexos — passe IDs
# ERRADO
@celery.task
def processar_usuario_errado(usuario): # objeto inteiro
pass
# CORRETO
@celery.task
def processar_usuario_correto(usuario_id: int): # apenas o ID
usuario = buscar_usuario_do_banco(usuario_id)
pass
# 4. Logging estruturado
@celery.task(bind=True, name="dados.importar")
def importar_dados(self, arquivo_id: int):
logger.info("Iniciando importação",
extra={"tarefa_id": self.request.id, "arquivo_id": arquivo_id})
Resumo
- Celery executa tarefas pesadas em segundo plano — libera a requisição HTTP imediatamente
- O Broker (Redis ou RabbitMQ) transporta mensagens; o Backend armazena resultados
task.delay()enfileira com argumentos posicionais;apply_async()oferece mais controlebind=Truedá acesso aself— permite retry, update_state e metadados da tarefa- Filas separadas (
padrao,prioritaria,pesada) permitem workers especializados - Celery Beat agenda tarefas com crontab — substitui cron jobs do sistema operacional
chain,groupechordcompõem workflows complexos de tarefas interdependentes- Tarefas devem ser idempotentes — seguras para reexecução em caso de falha
- Flower oferece monitoramento visual em tempo real de workers e tarefas
Referências e Leituras Complementares
- Celery — documentação oficial — https://docs.celeryq.dev/en/stable/
- Celery com FastAPI — guia prático — https://testdriven.io/blog/fastapi-and-celery/
- Flower — monitoramento — https://flower.readthedocs.io/en/latest/
- Redis como broker — https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html
- Canvas — workflows com chain, group, chord — https://docs.celeryq.dev/en/stable/userguide/canvas.html
- PERCIVAL, Harry; GREGORY, Bob. Architecture Patterns with Python. O'Reilly Media, 2020. Cap. 11 — event-driven architecture e processamento assíncrono.
- GRIGORIK, Ilya. High Performance Browser Networking. O'Reilly Media, 2013. — contexto de performance para entender quando usar processamento assíncrono