
O que é idempotência — e por que ela salvou meu sistema
Meu webhook de integração processava o mesmo evento duas vezes quando a rede ficava instável. Aprendi que o problema não era de rede — era que meu código não era idempotente.
O webhook recebia evento de criação de lead. Eu processava o lead e mandava para o CRM.
Em dias de rede instável, o servidor de origem não tinha certeza se a requisição tinha chegado — então enviava o mesmo evento de novo. Resultado: lead duplicado no CRM, cliente ligando perguntando por que tinha dois registros iguais.
Tentei resolver no lado da rede. Aumentei timeout. Não resolveu.
O problema não era na rede. Era que meu código não sabia o que fazer quando recebia o mesmo evento duas vezes.
O problema não era na rede. Era que meu handler de webhook criava um novo lead toda vez que recebia o evento. Segundo evento com mesmos dados = segundo lead. Não idempotente.
HTTP GET é idempotente — buscar o mesmo dado N vezes retorna o mesmo dado. HTTP POST geralmente não é — criar o mesmo recurso N vezes cria N recursos. Meu webhook era esse segundo caso.
A correção:
def handle_lead_created(event_data):
event_id = event_data["event_id"] # identificador único do evento
# verifica se já processamos esse evento
if ProcessedEvent.exists(event_id=event_id):
logger.info("evento duplicado ignorado", extra={"event_id": event_id})
return {"status": "already_processed"}
# processa o lead
lead = create_lead_in_crm(event_data)
# marca o evento como processado
ProcessedEvent.create(event_id=event_id, lead_id=lead.id)
return {"status": "ok", "lead_id": lead.id}
A tabela ProcessedEvent guarda os IDs de eventos já processados. Evento duplicado: ignora silenciosamente, retorna 200 (porque do ponto de vista do remetente, foi processado).
O pattern tem nome: idempotency key.
APIs de pagamento (Stripe, por exemplo) usam isso formalmente — você envia um Idempotency-Key header e o servidor garante que a mesma chave nunca processa duas vezes.
Em sistemas de webhook, você implementa a mesma ideia: cada evento tem um ID único, você deduplicha na entrada.
Tem uma versão mais simples para alguns casos: upsert em vez de insert.
INSERT INTO leads (email, source, created_at)
VALUES (%s, %s, NOW())
ON CONFLICT (email) DO UPDATE SET source = EXCLUDED.source
Se o email já existe, atualiza em vez de criar duplicata. É idempotente por natureza para esse caso específico.
Sistemas distribuídos falham de formas parciais. Request que sai pode não ter chegado. Request que chegou pode não ter sido respondida. A outra parte vai tentar de novo — é comportamento correto.
Código que não tolera retry duplo é código que assume que a rede é perfeita. A rede nunca é perfeita.
Idempotência não é feature avançada. É expectativa básica de qualquer sistema que recebe eventos de outro sistema. Quando aprendi isso, parei de culpar a rede e comecei a escrever código que tolera realidade.