Cover do episódio 112: O que é idempotência — e por que ela salvou meu sistema
#11218 de novembro, 20193 min leituraCódigo na PráticaS3 · 2018–2019

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.

IdempotênciaAPIsIntegraçãoQualidade

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.