Augusto Dalmas
notas de estudo
← voltar aos artigos
System Design  ·  30 jun 2026  ·  6 min de leitura

RabbitMQ + Python: o que acontece quando o worker falha antes do ack

Uma API que responde 201 antes de saber se o email foi enviado, e uma linha de código, requeue=False, que decide o destino de toda mensagem que falha.

Depois do estudo sobre cache, puxei outro assunto que aparece toda hora em sistemas maiores: filas de mensagem e comunicação assíncrona entre serviços.

O cenário

Montei um cenário direto: uma API em Node.js simula o registro de um usuário e, em vez de enviar o email ali mesmo, publica uma mensagem numa fila do RabbitMQ. Um worker em Python, separado e rodando de forma independente, fica ouvindo essa fila e é quem de fato dispara o email — usando o Resend.

Cliente -> API (Node.js) -> RabbitMQ -> Worker (Python + Pika) -> Resend (Email)

Duas linguagens diferentes conversando através de uma fila, sem uma conhecer a outra diretamente. Isso já era um primeiro ponto interessante: o RabbitMQ não se importa em qual linguagem o produtor ou o consumidor foram escritos, contanto que ambos falem o protocolo AMQP.

O lado que publica

./src/config/rabbitMQ.js

const rabbit = {
    send: async (queue, message) => {
        try {
            const connection = await amqp.connect(process.env.RABBIT_URL)
            const channel = await connection.createChannel()

            await channel.assertQueue(queue, { durable: true })

            channel.sendToQueue(
                queue,
                Buffer.from(JSON.stringify(message)),
                { persistent: true }
            )

            await new Promise(resolve => setTimeout(resolve, 500))

            await channel.close()
            await connection.close()
            return true
        } catch (error) {
            console.error(error)
            throw error
        }
    }
}

E o controller que usa isso:

./src/controller/userController.js

const user_register = {
    register: async function (req, res) {
        const { email } = req.body;

        try {
            const message = { email }
            await send('register', message)
            return res.status(201).json({ 'message': "Usuário criado com sucesso!" })
        } catch (error) {
            return res.status(500).json({ 'error': error.message })
        }
    }
}

O ponto central: a API responde 201 assim que a mensagem é publicada na fila — ela não espera o email ser enviado, nem sabe se o worker está de pé, nem sabe se o Resend vai aceitar o envio. Publicar na fila e mandar o email pro usuário são dois eventos completamente desacoplados no tempo. É basicamente a definição de comunicação assíncrona, mas só bateu de verdade quando eu vi a resposta 201 chegando instantânea mesmo com o worker desligado — a mensagem simplesmente fica esperando na fila (durable: true garante isso mesmo se o RabbitMQ reiniciar).

O lado que consome

./python-worker.py

channel.queue_declare(queue="register", durable=True)

def callback(ch, method, properties, body):
    try:
        data = json.loads(body)
        enviar_email(data['email'])
        ch.basic_ack(delivery_tag=method.delivery_tag)
    except Exception as e:
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

channel.basic_qos(prefetch_count=1)
channel.basic_consume(queue="register", on_message_callback=callback, auto_ack=False)

O comportamento em caso de falha depende de uma flag: auto_ack=False. Com ack manual, a mensagem só sai da fila quando o worker chama basic_ack explicitamente, depois de processar com sucesso — não quando o RabbitMQ simplesmente entrega ela. Isso existe pra cobrir o caso do worker morrer no meio do processamento: se ele cair antes do ack, o RabbitMQ devolve a mensagem pra fila pra outro consumidor tentar de novo.

O prefetch_count=1 também faz sentido nesse contexto: ele diz pro RabbitMQ “não me manda uma mensagem nova enquanto eu não confirmar a anterior”, o que evita um worker travado acumulando um monte de mensagens que ele nunca vai processar.

requeue=False: a linha que descarta mensagens

No except do callback, o código chama basic_nack(..., requeue=False). Se enviar_email() falhar por qualquer motivo — Resend fora do ar, chave de API inválida, o que for — a mensagem é descartada: não volta pra fila, não é tentada de novo, não vai pra lugar nenhum.

durable e persistent protegem a mensagem contra o RabbitMQ cair ou reiniciar — mas não protegem contra uma falha de negócio no consumidor. Isso é uma decisão que cabe a quem escreve o worker, e nesse projeto ela foi tomada priorizando simplicidade: falhou, descarta, segue o baile.

Limitações que ficaram evidentes

  • Não existe dead letter queue configurada. Uma fila de mensagens mortas seria o lugar natural pra onde essas mensagens que falharam deveriam ir, em vez de serem descartadas silenciosamente.
  • Sem retry: uma falha temporária no Resend (um timeout, por exemplo) tem o mesmo destino que um erro permanente — a mensagem morre igual.
  • Cada chamada a send() abre uma conexão AMQP nova com amqp.connect() e fecha tudo no fim, com uma pausa fixa de 500ms antes de fechar. Isso funciona, mas é caro: numa API com volume real, o ideal seria manter uma conexão/canal já abertos e reutilizá-los entre requisições, em vez de recriar isso a cada publicação.

Próximo passo

O caminho natural aqui seria configurar uma dead letter exchange pra capturar essas mensagens que falharam, em vez de descartá-las — e daí decidir o que fazer com elas (retry com backoff, alerta manual, etc).

#rabbitmq #filas #python #nodejs #arquitetura