Panorama — Celery vs RQ vs aio-pika vs aiokafka

TL;DR

Python tem quatro famílias de ferramentas pra tirar trabalho do caminho síncrono de uma requisição, e elas não competem entre si — resolvem problemas diferentes. Celery é a task queue madura e abstrata: você marca uma função com @app.task, chama .delay(), e o framework cuida de serializar, enfileirar (sobre Redis ou RabbitMQ), rotear pra um worker e — se configurado — reexecutar em caso de falha. RQ faz a mesma coisa com uma fração da superfície: só Redis, API menor, menos mágica, mais fácil de ler o código-fonte inteiro num fim de tarde. aio-pika abandona a abstração de “tarefa”: é um cliente assíncrono que fala AMQP direto com o RabbitMQ — você declara exchange, queue, binding, publica e consome, e implementa retry/idempotência com as próprias mãos. aiokafka (e sua prima síncrona kafka-python) não é task queue nenhuma: é cliente Kafka pra event streaming — múltiplos consumers lendo o mesmo log, não um único worker pegando uma tarefa e descartando-a. A pergunta que decide qual usar: você quer que algo aconteça em background (task queue: Celery/RQ) ou você quer controle fino sobre um broker, ou um log de eventos que vários consumers leem (comunicação direta: aio-pika/aiokafka)?

Um time está construindo a API de cadastro de uma plataforma de cursos. O fluxo de “criar conta” é direto: validar o formulário, gravar o usuário no banco, e mandar um e-mail de boas-vindas com um link de confirmação. No protótipo, o código é o que qualquer um escreveria primeiro:

@app.post("/usuarios", status_code=201)
def criar_usuario(dados: CriarUsuarioDTO):
    usuario = repositorio.salvar(Usuario.criar(dados))
    enviar_email_boas_vindas(usuario.email)  # chama o servidor SMTP, aqui, agora
    return usuario

Funciona no ambiente de desenvolvimento, onde o servidor SMTP de teste responde em milissegundos. Em produção, contra o provedor de e-mail transacional real, enviar_email_boas_vindas leva entre 200ms e 2 segundos — às vezes mais, quando o provedor está sob carga. O handler HTTP, que devolveria 201 Created em 15ms se só gravasse o usuário no banco, agora segura a conexão aberta esperando uma chamada de rede pra um serviço terceiro que não tem nada a ver com a pergunta “o usuário foi criado com sucesso?“. Pior: se o SMTP cair ou responder devagar sob pico de tráfego, o cadastro inteiro trava — o serviço de terceiros virou, sem ninguém decidir isso deliberadamente, uma dependência crítica do caminho mais importante da aplicação.

A pergunta certa não é “como faço o envio de e-mail ficar mais rápido” — é “por que o handler HTTP está esperando o SMTP responder, se a resposta HTTP não depende do resultado do e-mail?“. O cliente que chamou POST /usuarios não precisa saber se o e-mail já foi entregue — precisa saber se a conta foi criada. Isso é o problema canônico que motiva desacoplar: tirar do caminho síncrono qualquer trabalho cujo resultado não faz parte do contrato imediato da resposta.

Esta nota é o mapa: quatro ferramentas, quatro respostas diferentes pra “e se eu não quiser mais esperar o SMTP responder”, com o mesmo problema de e-mail de boas-vindas resolvido nas quatro, lado a lado, pra você comparar o código real, não só a descrição de marketing de cada uma.

O eixo que decide: tarefa a executar vs fato a registrar

Antes de comparar ferramentas, vale nomear a pergunta que já foi respondida em profundidade — de forma agnóstica de linguagem — em Comunicação entre Sistemas — Message queue vs event streaming: você está lidando com uma tarefa (processe isso uma vez, o resultado importa pra quem pediu, depois pode esquecer) ou com um fato (aconteceu algo que potencialmente múltiplos serviços, hoje ou no futuro, vão querer ler)?

“Enviar e-mail de boas-vindas” é claramente uma tarefa: alguém pede, o trabalho acontece uma vez, ninguém precisa “reler” esse evento depois. É exatamente o caso de uso onde task queues — Celery e RQ — foram desenhadas pra brilhar: elas abstraem a fila por trás de uma API de “chame esta função depois”, com retry, agendamento e resultado opcional embutidos.

Mas nem todo problema de desacoplamento em Python é uma tarefa fire-and-forget. Às vezes você precisa de controle fino sobre roteamento de mensagens (múltiplas filas, prioridades, exchanges do tipo topic) que a abstração de tarefa esconde de propósito — aí entra aio-pika, falando AMQP puro. E às vezes o que você tem não é uma tarefa, é um evento de negócio que três serviços diferentes (notificações, analytics, auditoria) precisam consumir de forma independente, com possibilidade de replay — aí a ferramenta certa não é task queue nenhuma, é um cliente Kafka: aiokafka.


flowchart TD
    A["Preciso tirar trabalho<br/>do caminho síncrono"] --> B{"É uma tarefa a executar<br/>uma vez, com retry,<br/>ou um fato que múltiplos<br/>consumers vão reagir/reler?"}
    B -->|"Tarefa (fire-and-forget)"| C{"Já tem RabbitMQ<br/>na stack, ou precisa<br/>de scheduling maduro<br/>(Beat)?"}
    C -->|"Sim"| D["Celery<br/>(Redis ou RabbitMQ)"]
    C -->|"Não, só Redis,<br/>quero simplicidade"| E["RQ"]
    B -->|"Controle fino direto<br/>com o broker AMQP"| F["aio-pika<br/>(assíncrono, RabbitMQ)"]
    B -->|"Fato/evento — múltiplos<br/>consumer groups, replay"| G["aiokafka / kafka-python<br/>(event streaming)"]

    style D fill:#4A90D9,color:#fff
    style E fill:#4A90D9,color:#fff
    style F fill:#F5A623,color:#000
    style G fill:#F5A623,color:#000

Resumo em uma frase: se a pergunta é “quero que isso aconteça depois, sem me importar como”, é task queue (Celery/RQ); se a pergunta é “quero falar com o broker diretamente, com controle total sobre roteamento ou sobre um log de eventos”, é comunicação direta (aio-pika/aiokafka) — e essa segunda categoria não é “Celery mais difícil”, é uma categoria de problema diferente.

Celery — a task queue madura e abstrata

Celery existe desde 2009 e é, de longe, a task queue mais adotada no ecossistema Python — o framework padrão citado em qualquer discussão sobre background jobs em Django, FastAPI ou Flask (Celery Project, Celery: Distributed Task Queue, 2026). A ideia central: você define uma função Python comum, decora com @app.task, e ganha de graça um jeito de chamá-la de forma assíncrona.

# tasks.py
from celery import Celery
 
app = Celery("cursos", broker="redis://localhost:6379/0")
 
@app.task
def enviar_email_boas_vindas(email: str) -> None:
    smtp_client.enviar(
        destinatario=email,
        assunto="Bem-vindo!",
        corpo="Sua conta foi criada.",
    )
# handler HTTP — mesma API, resolvendo o mesmo problema da abertura
@app.post("/usuarios", status_code=201)
def criar_usuario(dados: CriarUsuarioDTO):
    usuario = repositorio.salvar(Usuario.criar(dados))
    enviar_email_boas_vindas.delay(usuario.email)  # enfileira e retorna imediatamente
    return usuario

.delay(usuario.email) é açúcar sintático para .apply_async(args=(usuario.email,)) — serializa os argumentos (JSON por padrão), publica uma mensagem no broker (Redis ou RabbitMQ) e retorna na hora, sem esperar a tarefa executar. Um processo worker completamente separado — rodando com celery -A tasks worker, tipicamente em outro contêiner ou outra máquina — puxa a mensagem da fila e executa enviar_email_boas_vindas de verdade.

O que o Celery abstrai por baixo:

  • Serialização — argumentos e retorno viram JSON (ou pickle, msgpack — configurável) automaticamente; cuidado com objetos não serializáveis (conexões de banco, sessões HTTP) passados como argumento.
  • Roteamento.apply_async(queue="emails") manda a tarefa pra uma fila nomeada, permitindo workers dedicados por tipo de trabalho.
  • Agendamento.apply_async(countdown=60) ou .apply_async(eta=daqui_a_uma_hora) atrasa a execução sem código extra.
  • Resultado assíncrono — se um result backend estiver configurado (Redis, banco), AsyncResult permite consultar o estado (PENDING, SUCCESS, FAILURE) e o retorno da tarefa depois.
  • Retry declarativo e Celery Beat (tarefas periódicas, tipo cron) — cobertos com profundidade na nota 03 deste galho.

Celery não exige RabbitMQ

É comum ver “Celery = RabbitMQ” em tutoriais antigos, mas desde as primeiras versões o Celery suporta Redis como broker — mais simples de operar quando você já usa Redis pra cache, e suficiente pra maioria dos casos que não precisam de roteamento AMQP avançado (exchanges topic/fanout). RabbitMQ continua sendo a escolha certa quando você precisa de garantias de entrega mais fortes ou roteamento complexo — ver a comparação de brokers em Message queue vs event streaming.

Vale notar que a app Celery costuma crescer sua própria configuração central conforme o projeto amadurece — roteamento por padrão de nome de tarefa, timezone do Beat, serializer explícito em vez do padrão:

app.conf.update(
    task_routes={"tasks.gerar_certificado": {"queue": "certificados"}},
    task_serializer="json",
    result_serializer="json",
    accept_content=["json"],
    timezone="America/Sao_Paulo",
)

Esse bloco de configuração central é normal e esperado — é onde a “mágica” do Celery vira explícita, e é também o primeiro lugar a olhar quando uma tarefa se comporta de forma inesperada em produção.

O preço da abstração: Celery tem uma superfície de configuração grande — result backend, serializer, acks_late, task_reject_on_worker_lost, worker_prefetch_multiplier, dezenas de outras opções — e depurar “por que essa tarefa não rodou” às vezes exige entender várias camadas (broker, worker, prefetch, roteamento) ao mesmo tempo. É o trade-off central do framework: você ganha muito de graça, mas quando algo foge do caminho feliz, o “muito” vira superfície de investigação.

RQ — a fila simples sobre Redis

RQ (Redis Queue) nasceu como reação deliberada à complexidade do Celery: “simple job queues for Python”, como o próprio projeto se descreve (python-rq.org, RQ: Simple job queues for Python, 2026). A decisão de design mais importante do RQ é também sua maior limitação: só funciona sobre Redis, sem suporte a RabbitMQ ou outro broker.

# tasks.py — mesma função, sem decorator nenhum
def enviar_email_boas_vindas(email: str) -> None:
    smtp_client.enviar(
        destinatario=email,
        assunto="Bem-vindo!",
        corpo="Sua conta foi criada.",
    )
# handler HTTP
from redis import Redis
from rq import Queue
 
fila = Queue(connection=Redis())
 
@app.post("/usuarios", status_code=201)
def criar_usuario(dados: CriarUsuarioDTO):
    usuario = repositorio.salvar(Usuario.criar(dados))
    fila.enqueue(enviar_email_boas_vindas, usuario.email)  # sem decorator na função
    return usuario

A diferença mais visível de cara: enviar_email_boas_vindas não precisa de decorator nenhum — é uma função Python comum, e quem decide que ela vira um job é quem chama fila.enqueue(...). Isso é uma escolha deliberada de design: qualquer função importável (não uma lambda, não um método de instância sem cuidado extra) pode ser enfileirada, sem acoplar o módulo da tarefa a um objeto app do Celery.

O worker roda com rq worker (lendo a fila padrão) e executa cada job num processo filho — por padrão, um fork por job, o que isola falhas (um job que trava a memória não derruba os próximos) ao custo de mais overhead que o modelo de pool de threads/processos do Celery.

RQ não tem retry automático sofisticado nem scheduling nativo maduro

O que acontece: um time troca Celery por RQ esperando o mesmo comportamento de retry (autoretry_for, backoff exponencial) e tarefas periódicas (Beat) — e descobre, em produção, que RQ trata isso como responsabilidade da aplicação, não do framework. Por quê: é a decisão de design central do RQ — menos “mágica” significa menos features embutidas. RQ tem um decorator @job opcional pra retry simples desde versões recentes, e o pacote irmão rq-scheduler cobre agendamento, mas nenhum dos dois tem a profundidade das opções nativas do Celery (retry_backoff_max, retry_jitter, cron completo via Beat). Como evitar: decidir antes se o projeto vai precisar de retry com backoff sofisticado e agendamento tipo-cron nativo — se sim, Celery paga esse investimento inicial maior; se o projeto é pequeno e a maior parte dos jobs é fire-and-forget simples, a simplicidade do RQ (menos infraestrutura, código mais fácil de ler do início ao fim numa tarde) compensa.

O ganho real do RQ não é performance nem features — é debugabilidade. A base de código do RQ é pequena o suficiente pra ler inteira; quando uma tarefa se comporta de forma inesperada, dá pra rastrear o caminho completo (enqueue → serialização → worker → execução) sem precisar entender uma dúzia de camadas de configuração. Times pequenos, ou projetos onde a complexidade operacional do Celery (result backend, roteamento multi-fila, Beat) não se paga, tendem a preferir RQ justamente por isso.

aio-pika — falando AMQP direto, sem abstração de tarefa

Celery e RQ têm algo em comum apesar das diferenças: os dois escondem o broker atrás de uma API de “tarefa”. Você nunca declara uma exchange, nunca faz um binding manual, nunca decide o nome exato de uma fila — o framework decide isso por você. aio-pika joga essa abstração fora deliberadamente: é um cliente assíncrono (asyncio) que fala o protocolo AMQP 0-9-1 direto com o RabbitMQ, dando acesso total aos conceitos do broker — exchange, queue, binding, routing key — sem nenhuma camada de “tarefa” no meio (aio-pika docs, AsyncIO client for RabbitMQ, 2026).

import aio_pika
 
async def enviar_email_boas_vindas(email: str) -> None:
    connection = await aio_pika.connect_robust("amqp://guest:guest@localhost/")
    async with connection:
        channel = await connection.channel()
        exchange = await channel.declare_exchange("emails", aio_pika.ExchangeType.DIRECT)
        await exchange.publish(
            aio_pika.Message(body=email.encode()),
            routing_key="boas-vindas",
        )
# handler HTTP — assíncrono, precisa de um framework async (FastAPI, por exemplo)
@app.post("/usuarios", status_code=201)
async def criar_usuario(dados: CriarUsuarioDTO):
    usuario = await repositorio.salvar(Usuario.criar(dados))
    await enviar_email_boas_vindas(usuario.email)  # publica e retorna, não é "delay"
    return usuario

Note a diferença estrutural: não existe .delay(). Você abre uma conexão, declara (ou referencia) uma exchange, e publica uma mensagem com uma routing key — os mesmos conceitos cobertos, de forma agnóstica de linguagem, em Comunicação entre Sistemas — Mensageria. connect_robust() é a função mais citada da biblioteca: abre uma conexão com reconexão automática — se o RabbitMQ cair e voltar, o cliente reconecta e redeclara topologia sozinho, o que evita boa parte do código de resiliência que você teria que escrever à mão com um cliente AMQP mais cru.

O lado consumidor é igualmente direto:

async def consumir_emails():
    connection = await aio_pika.connect_robust("amqp://guest:guest@localhost/")
    async with connection:
        channel = await connection.channel()
        await channel.set_qos(prefetch_count=10)
        queue = await channel.declare_queue("fila-emails", durable=True)
 
        async with queue.iterator() as fila_iter:
            async for mensagem in fila_iter:
                async with mensagem.process():  # ack automático ao sair do bloco
                    smtp_client.enviar(destinatario=mensagem.body.decode(), ...)

Com aio-pika, retry e idempotência são seu problema

O que acontece: um time migra de Celery pra aio-pika esperando manter o mesmo comportamento de retry automático em caso de falha — e descobre que, sem o decorator @app.task e as opções autoretry_for/retry_backoff do Celery, nada reenvia a mensagem sozinho. Por quê: é exatamente a troca que motiva escolher aio-pika em primeiro lugar — controle total sobre o comportamento do broker significa que nenhum comportamento vem de graça. Se mensagem.process() levantar uma exceção, cabe ao código decidir se faz nack com requeue, publica numa dead-letter queue, ou descarta. Como evitar: só optar por aio-pika quando o controle fino (routing complexo, prioridades, protocolos além de “task simples”) vale o código extra de gerenciar retry e confirmação manualmente — a nota 07 deste galho mostra DLQ e idempotência aplicados com código real.

async/await aqui pressupõe familiaridade com o modelo de concorrência do asyncio — event loop, corrotinas, async with — já coberto nos Galhos 7-8 desta trilha e não reexplicado aqui; aio-pika só usa esse modelo, não o reensina.

aiokafka e kafka-python — não é task queue, é event streaming

A quarta ferramenta muda de categoria inteira. kafka-python (cliente síncrono) e aiokafka (cliente assíncrono, asyncio-nativo) falam com o Apache Kafka — e Kafka não é um broker de tarefas, é um log de eventos (aiokafka docs, aiokafka: AsyncIO Kafka client, 2026). A diferença de propósito em relação a Celery/RQ/aio-pika não é sutil: aqui, uma mensagem publicada não desaparece ao ser consumida — ela fica no log, e múltiplos consumer groups independentes podem lê-la, cada um no seu próprio ritmo, com replay disponível.

Se o problema fosse só “enviar o e-mail de boas-vindas”, Kafka seria uma escolha estranha — over-engineering pra uma tarefa simples de fire-and-forget. O cenário onde aiokafka faz sentido é diferente: múltiplos serviços reagindo ao mesmo fato. Suponha que “usuário criado” não deva só disparar um e-mail — deva também alimentar um pipeline de analytics e um serviço de recomendação de cursos, cada um lendo o evento de forma independente, e potencialmente reprocessando o histórico depois:

from aiokafka import AIOKafkaProducer
import json
 
async def publicar_usuario_criado(usuario: Usuario) -> None:
    producer = AIOKafkaProducer(bootstrap_servers="localhost:9092")
    await producer.start()
    try:
        evento = {"tipo": "usuario_criado", "email": usuario.email, "id": str(usuario.id)}
        await producer.send_and_wait("usuarios.eventos", json.dumps(evento).encode())
    finally:
        await producer.stop()
# handler HTTP
@app.post("/usuarios", status_code=201)
async def criar_usuario(dados: CriarUsuarioDTO):
    usuario = await repositorio.salvar(Usuario.criar(dados))
    await publicar_usuario_criado(usuario)  # publica um FATO, não agenda uma tarefa
    return usuario

Do lado consumidor, o serviço de e-mail e o serviço de analytics são dois consumer groups separados, lendo o mesmo tópico sem interferir um no outro:

from aiokafka import AIOKafkaConsumer
import json
 
async def consumir_para_emails():
    consumer = AIOKafkaConsumer(
        "usuarios.eventos",
        bootstrap_servers="localhost:9092",
        group_id="servico-emails",  # offset independente do grupo de analytics
    )
    await consumer.start()
    try:
        async for msg in consumer:
            evento = json.loads(msg.value)
            smtp_client.enviar(destinatario=evento["email"], ...)
    finally:
        await consumer.stop()

Trocar group_id="servico-emails" por group_id="servico-analytics" num segundo consumer, rodando em outro processo, é o bastante pra criar um segundo leitor completamente independente do mesmo stream — sem fila, sem competing consumers, sem coordenação entre os dois times. Isso é estruturalmente impossível de replicar com Celery/RQ sem reinventar um log por baixo, porque task queue remove a mensagem ao consumir; log não remove.

Conceitos como partição, consumer group, offset, e a distinção formal entre fila e streaming não são reexplicados aqui — moram em Message queue vs event streaming e em Comunicação entre Sistemas — Mensageria; esta nota assume que o modelo mental já está claro e foca em como o Python fala com ele. A nota 06 deste galho aprofunda KafkaProducer/KafkaConsumer síncronos vs AIOKafkaProducer/AIOKafkaConsumer, commit de offset manual vs automático, e (de)serialização.

Instalação mínima

Antes de comparar código de aplicação, vale ver o que cada ferramenta exige só para existir no projeto — a diferença de superfície já aparece aqui, antes de qualquer linha de lógica de negócio.

# Celery — broker Redis (mais simples de operar que RabbitMQ para começar)
pip install celery[redis]
# RQ — só Redis, sem opção de outro broker
pip install rq
# aio-pika — cliente AMQP assíncrono
pip install aio-pika
# aiokafka — cliente Kafka assíncrono; kafka-python é a alternativa síncrona
pip install aiokafka

Nos quatro casos o broker em si (Redis, RabbitMQ, Kafka) roda como serviço externo — nenhuma das bibliotecas embute um broker; pip install só traz o cliente. Para desenvolvimento local, subir o broker via docker compose (uma imagem redis:7, rabbitmq:4-management ou apache/kafka:4.0) é o caminho mais rápido — os brokers em si, com profundidade, estão cobertos em Comunicação entre Sistemas — Mensageria, não aqui.

FerramentaPacote PyPIDepende de
Celerycelery[redis] ou celery[librabbitmq]Redis ou RabbitMQ (externo)
RQrqRedis (externo)
aio-pikaaio-pikaRabbitMQ (externo)
aiokafkaaiokafkaKafka (externo)

Comparando o código lado a lado

O mesmo problema — “criar usuário, desacoplar o envio de e-mail do handler HTTP” — resolvido nas quatro ferramentas, resumido:

FerramentaChamada no handlerO que acontece por baixo
Celeryenviar_email_boas_vindas.delay(usuario.email)Serializa, publica no broker (Redis/RabbitMQ), worker separado executa; retry/agendamento/resultado de graça
RQfila.enqueue(enviar_email_boas_vindas, usuario.email)Publica no Redis (só Redis); worker faz fork por job; retry e scheduling exigem extensões
aio-pikaawait exchange.publish(msg, routing_key=...)Publica direto numa exchange AMQP; você declara topologia, gerencia ack/retry manualmente
aiokafkaawait producer.send_and_wait("usuarios.eventos", evento)Anexa ao log Kafka; múltiplos consumer groups leem de forma independente, com replay

Tabela de decisão

CritérioCeleryRQaio-pikaaiokafka
Modelo mentalTask queueTask queueCliente AMQP diretoCliente de log/streaming
Broker(s)Redis ou RabbitMQSó RedisRabbitMQKafka
Abstração de “tarefa”Sim (@app.task)Sim (função comum + enqueue)Não — você fala com exchange/queueNão — você publica/consome eventos
Retry automáticoSim, configurável (autoretry_for)Básico (@job, extensões)ManualManual (nível de aplicação)
Scheduling nativoSim (Celery Beat)Só com rq-schedulerNãoNão
Múltiplos consumers independentes do mesmo dadoNão (competing consumers)Não (competing consumers)Não, por padrão AMQPSim (consumer groups)
Replay de históricoNãoNãoNãoSim
Curva de aprendizado / superfícieMédia-altaBaixaMédia (exige conhecer AMQP)Média-alta (exige conhecer Kafka)
Caso de uso canônicoBackground jobs em geral, envio de e-mail/notificação, processamento pesadoMesmo que Celery, projetos menores, times que preferem menos “mágica”Roteamento complexo, controle fino, RPC assíncronoEvent-driven architecture, analytics, auditoria, CDC

A pergunta de uma frase

“Quero que algo aconteça em background, sem me importar como” → task queue (Celery se precisar de RabbitMQ/scheduling maduro; RQ se só tem Redis e quer simplicidade). “Quero controle fino sobre o broker, ou um log de eventos que múltiplos serviços leem de forma independente” → comunicação direta (aio-pika pra RabbitMQ; aiokafka pra Kafka).

Casos práticos

A mesma plataforma de cursos, seis meses depois. O time do exemplo de abertura resolveu o problema do e-mail de boas-vindas com Celery sobre Redis — decisão correta para o volume da época, umas centenas de cadastros por dia. O produto cresce, e agora existem três tipos de trabalho em background rodando na mesma instalação Celery: envio de e-mail transacional (rápido, alto volume), geração de certificado em PDF ao concluir um curso (lento, CPU-bound, minutos em vez de segundos), e um relatório semanal de progresso por e-mail (agendado, uma vez por semana, para toda a base de alunos). Rodar os três tipos de tarefa no mesmo pool de workers é um erro sutil: um pico de geração de certificados no fim do mês pode saturar os workers e atrasar e-mails transacionais que deveriam ser quase instantâneos. A correção não muda de ferramenta — usa uma capacidade que já está lá: .apply_async(queue="certificados") roteia a tarefa pesada para uma fila nomeada, e um segundo grupo de workers (celery -A tasks worker -Q certificados) é dedicado só a ela, isolando o impacto. O relatório semanal usa Celery Beat, cravado com profundidade na nota 03. Nenhuma dessas três necessidades — roteamento por fila, isolamento de workers, agendamento — exigiu trocar de ferramenta; exigiu usar mais da superfície que o Celery já oferece.

Integração com um parceiro de pagamento via webhook. Um serviço de pagamentos externo notifica a aplicação, via HTTP, quando uma cobrança é confirmada ou recusada — e o contrato do parceiro exige responder 200 OK em menos de 3 segundos, sob pena de reenvio automático (e, depois de N reenvios, suspensão temporária do endpoint). Só gravar o evento no banco e devolver 200 já resolveria o requisito de latência, mas o processamento real — atualizar o status do pedido, notificar o cliente, disparar a nota fiscal — tem múltiplos passos, alguns deles chamando outros serviços internos que também podem falhar e precisar de retry independente. Esse é um caso onde aio-pika compensa o código extra: o handler do webhook só valida a assinatura e publica um evento cru numa exchange (pagamentos.eventos, tipo topic), com routing keys diferentes por tipo de evento (pagamento.confirmado, pagamento.recusado). Consumers dedicados — um para atualização de pedido, outro para nota fiscal — assinam bindings diferentes da mesma exchange, cada um com sua própria política de retry e sua própria dead-letter queue. Celery resolveria a parte de “rodar isso depois”, mas o roteamento por tipo de evento para múltiplos consumers especializados, cada um com sua fila de erro isolada, é exatamente o cenário onde falar AMQP diretamente compensa o controle extra.

Rastreamento de eventos de aprendizado para dois times diferentes. A mesma plataforma de cursos introduz um requisito novo: cada interação relevante do aluno (assistiu um vídeo até o fim, completou um exercício, abandonou um módulo no meio) deve alimentar dois sistemas completamente independentes — o motor de recomendação de próximos cursos (lê o histórico completo, batch, uma vez por dia) e um dashboard de analytics em tempo quase real para o time de conteúdo (lê o evento assim que acontece). Modelar isso como tarefas Celery — uma tarefa “processar evento” — funcionaria para um dos dois consumidores, mas não para os dois ao mesmo tempo sem truques (publicar duas tarefas idênticas toda vez que um evento acontece, o que acopla quem produz o evento a saber quantos e quais consumidores existem hoje). Isso é o sinal clássico de que o problema mudou de categoria: não é mais “execute isso uma vez”, é “registre este fato para quem quiser ler, hoje ou daqui a meses” — o motor de recomendação inclusive quer reprocessar o histórico completo sempre que o algoritmo mudar, o que é replay de verdade. aiokafka resolve isso naturalmente: um único producer.send() por evento de aprendizado, publicado no tópico aprendizado.eventos, com dois consumer groups (motor-recomendacao e dashboard-analytics) lendo o mesmo stream de forma totalmente independente — nenhum dos dois sabe que o outro existe, e um terceiro consumidor pode aparecer amanhã sem que o código que publica o evento mude uma linha.

Armadilhas comuns

Usar Celery para um caso de event streaming

O que acontece: um time modela “usuário criado” como uma tarefa Celery (publicar_usuario_criado.delay(...)) mesmo sabendo que três serviços diferentes (e-mail, analytics, recomendação) precisam reagir ao mesmo evento — e descobre depois que Celery entrega a tarefa a um worker, não a três. Por quê: Celery é, por baixo, uma fila de competing consumers — cada mensagem vai pra um worker só, o mesmo modelo de “message queue” descrito em Message queue vs event streaming. Simular fan-out com Celery exige truques (publicar N tarefas, uma por consumidor conhecido) que ficam frágeis assim que um quinto serviço precisa começar a reagir ao mesmo evento sem que o código que publica precise saber disso. Como evitar: nomear explicitamente, antes de escrever código, se o cenário é “uma tarefa, um executor” (task queue) ou “um fato, N leitores independentes, possivelmente no futuro” (streaming) — a resposta decide entre Celery/RQ e aiokafka antes de qualquer linha de implementação.

Escolher aio-pika achando que é "Celery mais rápido"

O que acontece: um time troca Celery por aio-pika esperando ganhar performance, e se surpreende com o volume de código novo — declarar exchange, gerenciar reconexão, implementar retry manual — pra reproduzir o que o Celery já fazia de graça. Por quê: aio-pika não é uma versão mais rápida do Celery — é uma ferramenta de categoria diferente, que troca conveniência por controle. Ganha-se em previsibilidade e em acesso a recursos do AMQP que o Celery não expõe (roteamento topic/fanout complexo, prioridade nativa de fila), mas perde-se tudo que o Celery abstrai. Como evitar: só escolher aio-pika quando o motivo for controle explícito sobre o broker — não performance genérica, que Celery sobre Redis já entrega bem pra volume moderado.

Achar que RQ é "Celery com menos funcionalidade" e nada mais

O que acontece: um time avalia RQ só pela lista de features que falta (sem Beat robusto, sem múltiplos brokers) e descarta sem considerar o ganho real, que é debugabilidade e simplicidade operacional. Por quê: a comparação “quantas features tem” ignora o custo operacional de manter e depurar a superfície do Celery quando o projeto não precisa da maior parte dela — result backend configurado sem uso real, roteamento multi-fila que nunca é usado, opções de retry nunca ajustadas do padrão. Como evitar: avaliar RQ pelo que o projeto de fato precisa hoje, não pela lista de features do concorrente — projetos pequenos a médios, com Redis já na stack, ganham mais em simplicidade do que perdem em features não usadas.

Observabilidade: como saber que a tarefa realmente rodou

Um problema que aparece igual nas quatro ferramentas, embora resolvido de formas diferentes: depois que o handler HTTP devolve 201 e segue em frente, como alguém descobre que enviar_email_boas_vindas de fato executou — ou falhou silenciosamente?

  • Celery tem o ecossistema mais maduro nesse ponto: Flower, um dashboard web (pip install flower, celery -A tasks flower) mostra tarefas em tempo real, taxa de sucesso/falha, tempo de execução por worker, e permite inspecionar o traceback de uma tarefa que falhou — sem precisar caçar logs espalhados. É o mesmo tipo de ganho que motiva escolher Celery em primeiro lugar: muita coisa vem de graça, aqui incluindo visibilidade operacional.
  • RQ tem o rq-dashboard, um projeto irmão bem mais simples que o Flower — lista de filas, jobs em andamento, jobs falhados com traceback. Cobre o essencial sem a profundidade de métricas do Flower, consistente com a filosofia geral do RQ.
  • aio-pika não tem dashboard de aplicação — a visibilidade vem do RabbitMQ Management Plugin, que mostra filas, taxa de mensagens, consumers conectados, mas não sabe nada sobre “tarefas” ou o que uma mensagem representa semanticamente. Rastrear se um e-mail específico foi enviado com sucesso é responsabilidade do código da aplicação — normalmente via logging estruturado e métricas próprias (Prometheus, por exemplo).
  • aiokafka também depende de ferramentas externas ao cliente Python — Kafka expõe métricas de lag de consumer group (quão atrás um consumer group está do fim do log), essenciais para saber se um serviço está processando eventos no ritmo em que são produzidos, mas essas métricas vêm do broker e de ferramentas de operação (Kafka Manager, Prometheus JMX Exporter), não de uma biblioteca Python.

O padrão gera menos observabilidade de graça, não mais controle sobre observabilidade

É tentador ler “aio-pika e aiokafka dão mais controle” como “e, portanto, mais fácil de observar” — é o oposto. Controle fino sobre o broker significa que a aplicação também é responsável por instrumentar o que a task queue instrumentaria de graça. Isso não é motivo para nunca escolher aio-pika/aiokafka, mas é um custo real a somar na decisão, não só o custo de escrever mais código de publish/consume.

Em entrevista

Uma pergunta comum em entrevistas sênior que tocam sistemas Python é “como você desacoplaria uma chamada lenta (SMTP, terceiros) do caminho de resposta de uma API?“. A resposta que sinaliza profundidade não pula direto pro nome de uma ferramenta — nomeia primeiro a categoria do problema: “primeiro eu pergunto se é uma tarefa fire-and-forget ou um evento que múltiplos serviços vão consumir de forma independente. Se for tarefa, uso uma task queue — Celery se já preciso de scheduling maduro ou RabbitMQ, RQ se o projeto é menor e só tem Redis disponível. Se for um evento que outros serviços vão reagir de forma independente, ou que preciso poder reprocessar depois, isso já não é task queue — é streaming, e eu uso um cliente Kafka como aiokafka. Confundir os dois é o erro mais comum que vejo: gente tentando fazer fan-out de evento com uma fila que só entrega a mensagem pra um consumer.”

Como explicar em inglês

“Python has two categories of tools for taking work off the synchronous request path. Task queues — Celery and RQ — abstract the broker behind a ‘run this function later’ API: you decorate or enqueue a function, and the framework handles serialization, routing to a worker, and often retry and scheduling. Celery supports both Redis and RabbitMQ as brokers and has a mature feature set — retries, periodic tasks via Celery Beat, result backends. RQ deliberately trades features for simplicity: Redis-only, a smaller codebase you can read end-to-end, less magic to debug. The other category is direct broker communication — aio-pika talks AMQP straight to RabbitMQ with no task abstraction, giving you full control over exchanges and routing at the cost of implementing retry and idempotency yourself. aiokafka is a different category entirely — it’s an event streaming client, not a task queue, so instead of one worker consuming and discarding a message, multiple independent consumer groups can read the same event log, with replay available. The decision isn’t ‘which is faster’ — it’s ‘do I want a task to happen once, or do I want a fact multiple services can react to independently?‘”

PTEN
Fila de tarefasTask queue
Trabalhador / consumidorWorker
Corretor / brokerBroker
SerializaçãoSerialization
Tentativa automáticaAutomatic retry
Tarefa agendada / periódicaScheduled / periodic task
Backend de resultadoResult backend
Cliente assíncronoAsynchronous client
Grupo de consumidoresConsumer group
ReprocessamentoReplay

O que vem a seguir

Este panorama respondeu “qual ferramenta pra qual problema” — as próximas notas do galho entram fundo em cada uma. A ordem segue a mesma lógica desta nota: primeiro as duas task queues (Celery com profundidade, depois RQ em contraste direto), depois os dois clientes de broker direto.

Veja também

Fontes

  • Celery Project — Celery: Distributed Task Queue (acessado 2026-07-12) — arquitetura de broker/worker, .delay()/.apply_async(), serialização, result backend.
  • python-rq.org — RQ: Simple job queues for Python (acessado 2026-07-12) — filosofia de design “simples sobre Redis”, enqueue(), workers.
  • aio-pika docs — AsyncIO client for RabbitMQ (acessado 2026-07-12) — connect_robust(), declaração de exchange/queue/binding, publish/consume assíncrono.
  • aiokafka docs — aiokafka: AsyncIO Kafka client (acessado 2026-07-12) — AIOKafkaProducer/AIOKafkaConsumer, consumer groups, comparação com kafka-python.
  • Real Python — Task Queues in Python (acessado 2026-07-12) — panorama de task queues Python, comparação Celery vs alternativas.