Levi Lael
Voltar para o blog
IA Aplicada09 de maio de 2026·10 min de leitura

Idempotência em Webhooks de IA: Por Que Importa em Produção

Webhooks sem idempotência em sistemas de IA são bombas-relógio: silenciosas até explodir em produção. Veja como blindar suas integrações com exemplos reais de código.

LL

Levi Lael

Engenheiro de operações com IA

Compartilhar

Seu sistema de IA processou o mesmo evento três vezes. Cobrou o cliente três vezes. Disparou três e-mails de confirmação. E você só ficou sabendo quando o suporte abriu um ticket na segunda-feira de manhã.

Esse cenário não é hipotético. Aconteceu comigo durante o desenvolvimento do CaixaHub, quando os webhooks do Open Finance começaram a chegar em duplicata durante instabilidades de rede. O provider reenvia. O load balancer reenvia. Seu próprio retry reenvia. Sem idempotência em webhooks de IA, cada reenvio é uma bomba silenciosa esperando explodir.

O problema fica pior quando IA entra na equação. Uma chamada à OpenAI ou Anthropic não é gratuita — nem em dinheiro, nem em latência. Processar o mesmo evento duas vezes significa pagar duas vezes, armazenar resultado duplicado, e possivelmente tomar decisões de negócio conflitantes com base em dados inconsistentes.

Neste artigo você vai aprender: o que exatamente torna um webhook handler idempotente, por que isso é crítico em fluxos que envolvem modelos de linguagem, e como implementar deduplicação robusta com retry exponencial em menos de 30 minutos. Com código real, sem abstração vaga.

O que "idempotente" significa de verdade (sem a teoria de faculdade)

Idempotência é uma propriedade matemática. Na prática: executar a mesma operação N vezes produz o mesmo resultado que executar uma vez.

f(f(x)) = f(x)

Em webhooks, isso significa: receber o mesmo evento duas vezes não muda o estado do sistema além do que a primeira execução já mudou.

Parece óbvio. Mas a maioria dos handlers que vejo em código de produção não é idempotente. Por quê? Porque o caminho natural de escrever código é linear: chega evento → processa → salva resultado. Ninguém pensa no reenvio até ele acontecer.

Alerta: Providers de webhook sérios (Stripe, GitHub, Open Finance) explicitamente documentam que vão reenviar eventos em caso de falha de entrega. Isso não é bug — é spec. Seu sistema precisa estar preparado.

O ciclo de vida de um evento reenviado

Quando um provider não recebe confirmação (HTTP 200) dentro do timeout, ele reenvia. Os motivos mais comuns:

  1. Seu servidor demorou pra responder (processamento pesado com IA)
  2. Deploy aconteceu durante o processamento
  3. Falha de rede entre provider e seu servidor
  4. Seu handler retornou 500 por erro interno

O problema específico com IA: chamadas a modelos grandes têm latência de 2 a 15 segundos facilmente. Muitos providers têm timeout de 5-10 segundos. Você responde devagar, o provider reenvia, você processa duas vezes. Essa é a armadilha mais comum em arquiteturas que adicionam IA a um webhook existente sem rearquitetar o handler.

Por que webhooks de IA têm o problema amplificado

Num webhook tradicional — digamos, atualização de status de pedido — processar duas vezes é ruim mas recuperável. Você sobrescreve com o mesmo valor, no máximo.

Em fluxos com IA, os efeitos colaterais são mais pesados:

Custo financeiro direto. Cada chamada à API da Anthropic ou OpenAI tem custo por token. Processar o mesmo documento 3x = pagar 3x. Em alto volume, isso corrói margem de forma invisível.

Efeitos colaterais assimétricos. Se seu webhook dispara um agente que envia uma proposta comercial, você não quer enviar a mesma proposta duas vezes pro mesmo lead. O dano à reputação é real.

Estado inconsistente em banco. Dois processos paralelos gerando e salvando resultados de IA para o mesmo evento podem criar registros conflitantes — especialmente se você usa INSERT em vez de UPSERT.

Consumo de rate limit. APIs de IA têm limites de requisição por minuto. Reprocessamentos desnecessários consomem esse limite, afetando requests legítimas.

A anatomia de um handler idempotente

A solução central é simples: antes de processar qualquer coisa, verificar se esse evento já foi processado. Se sim, retornar sucesso imediatamente sem executar nada.

Isso requer três componentes:

  1. Identificador único do evento (geralmente fornecido pelo provider)
  2. Store de deduplicação (Redis, banco de dados, qualquer coisa persistente)
  3. Lógica de check antes do processamento
python
import redis
import json
from functools import wraps
r = redis.Redis(host='localhost', port=6379, db=0)
IDEMPOTENCY_TTL = 86400 # 24 horas em segundos
def idempotent_handler(func):
@wraps(func)
def wrapper(event: dict, *args, **kwargs):
event_id = event.get("id") or event.get("event_id")
if not event_id:
# Sem ID, não dá pra garantir idempotência — log e processa
print(f"[WARN] Evento sem ID recebido: {event}")
return func(event, *args, **kwargs)
idempotency_key = f"webhook:processed:{event_id}"
# Tentativa atômica: só seta se não existir (NX)
was_set = r.set(idempotency_key, "1", ex=IDEMPOTENCY_TTL, nx=True)
if not was_set:
# Já processado — retorna sem fazer nada
print(f"[INFO] Evento duplicado ignorado: {event_id}")
return {"status": "already_processed", "event_id": event_id}
try:
return func(event, *args, **kwargs)
except Exception as e:
# Falhou — remove a chave pra permitir retry legítimo
r.delete(idempotency_key)
raise e
return wrapper

O detalhe crítico está na linha nx=True. O comando SET ... NX do Redis é atômico — só seta se a chave não existe. Isso evita race condition quando dois workers recebem o mesmo evento simultaneamente.

Outro detalhe: se o processamento falhou (exceção), a chave é deletada. Isso permite que o provider reenvie e o sistema tente de novo. Idempotência não significa "nunca tenta de novo" — significa "não processa o mesmo evento bem-sucedido duas vezes".

Implementando retry exponencial no lado do consumidor

Idempotência resolve o problema do reenvio externo. Mas você também precisa de retry interno — quando sua chamada à API de IA falha.

A regra de ouro: retry com backoff exponencial e jitter.

python
import time
import random
from anthropic import Anthropic, APIError, RateLimitError
client = Anthropic()
def call_ai_with_retry(
prompt: str,
max_retries: int = 4,
base_delay: float = 1.0,
max_delay: float = 60.0
) -> str:
for attempt in range(max_retries):
try:
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
except RateLimitError:
if attempt == max_retries - 1:
raise
# Rate limit: espera mais
delay = min(base_delay * (2 ** attempt) * 2, max_delay)
jitter = random.uniform(0, delay * 0.1)
time.sleep(delay + jitter)
except APIError as e:
if attempt == max_retries - 1:
raise
if e.status_code and e.status_code < 500:
# Erro 4xx: não vai melhorar com retry
raise
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1)
time.sleep(delay + jitter)
raise RuntimeError("Max retries exceeded")

O jitter — ruído aleatório adicionado ao delay — é fundamental em sistemas com múltiplos workers. Sem ele, todos os workers que falharam simultaneamente vão tentar de novo ao mesmo tempo, criando um thundering herd que piora o problema de rate limit.

Insight contrarian: A maioria dos tutoriais de webhook mostra o handler respondendo 200 após processar tudo. Em fluxos com IA, isso é erro de arquitetura. O correto é responder 200 imediatamente, enfileirar o processamento, e usar um worker assíncrono. O provider não precisa saber quanto tempo sua IA demorou.

Respondendo rápido, processando depois: o padrão correto

A arquitetura robusta para webhooks com IA tem duas fases separadas: recepção e processamento.

python
# Django view — responde em <50ms, sempre
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from .tasks import process_ai_webhook # Celery task
import json
import hmac
import hashlib
@csrf_exempt
def webhook_receiver(request):
if request.method != "POST":
return JsonResponse({"error": "Method not allowed"}, status=405)
# 1. Verificar assinatura (obrigatório)
signature = request.headers.get("X-Signature-256", "")
if not verify_signature(request.body, signature):
return JsonResponse({"error": "Invalid signature"}, status=401)
# 2. Parse mínimo — só o necessário pra identificar
try:
payload = json.loads(request.body)
event_id = payload.get("id")
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
# 3. Enfileira pra processamento assíncrono
process_ai_webhook.delay(event_id, payload)
# 4. Responde imediatamente — não espera IA terminar
return JsonResponse({"status": "queued", "event_id": event_id}, status=200)
def verify_signature(payload: bytes, signature: str) -> bool:
secret = b"seu_webhook_secret_aqui"
expected = "sha256=" + hmac.new(secret, payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
python
# tasks.py — o worker Celery que faz o trabalho pesado
from celery import shared_task
from .idempotency import idempotent_handler
from .ai import call_ai_with_retry
@shared_task(bind=True, max_retries=3)
def process_ai_webhook(self, event_id: str, payload: dict):
idempotency_key = f"webhook:processed:{event_id}"
# Check de idempotência dentro do worker também
if redis_client.exists(idempotency_key):
return {"status": "already_processed"}
try:
# Processamento real com IA
result = call_ai_with_retry(
prompt=build_prompt_from_payload(payload)
)
# Salva resultado
save_result_to_db(event_id, result)
# Marca como processado
redis_client.set(idempotency_key, "1", ex=86400, nx=True)
return {"status": "processed", "event_id": event_id}
except Exception as exc:
# Retry com backoff
raise self.retry(exc=exc, countdown=2 ** self.request.retries)

Esse padrão — receiver leve + worker assíncrono — resolve três problemas de uma vez: timeout do provider, retry seguro com idempotência, e separação de responsabilidades.

Deduplicação no banco de dados: a segunda linha de defesa

Redis pode falhar. Cache pode ser limpo. Você precisa de uma segunda camada de proteção no banco de dados.

sql
-- PostgreSQL: constraint unique no event_id
CREATE TABLE webhook_events (
id BIGSERIAL PRIMARY KEY,
event_id VARCHAR(255) NOT NULL UNIQUE,
payload JSONB NOT NULL,
processed_at TIMESTAMPTZ DEFAULT NOW(),
result JSONB
);
-- Índice pra performance em consultas
CREATE INDEX idx_webhook_events_event_id ON webhook_events(event_id);
python
from django.db import IntegrityError
from .models import WebhookEvent
def save_result_to_db(event_id: str, payload: dict, result: dict):
try:
WebhookEvent.objects.create(
event_id=event_id,
payload=payload,
result=result
)
except IntegrityError:
# Duplicate key — evento já foi salvo por outro worker
# Isso é esperado e correto
print(f"[INFO] Evento já existe no banco: {event_id}")
return

O UNIQUE no banco é o guard final. Se Redis falhou e dois workers processaram o mesmo evento em paralelo, o segundo vai receber IntegrityError no INSERT. Você trata isso como sucesso silencioso — o evento foi processado, só não por esse worker.

Quick Win: blindar um webhook existente em 30 minutos

Se você tem um webhook handler hoje que não tem idempotência, aqui está o passo a passo mínimo para adicionar:

Passo 1 — Instalar Redis client (2 min)

bash
pip install redis # Python
# ou
npm install ioredis # Node.js

Passo 2 — Adicionar coluna no banco (5 min)

sql
ALTER TABLE seu_modelo ADD COLUMN IF NOT EXISTS
webhook_event_id VARCHAR(255) UNIQUE;

Passo 3 — Criar função de check (10 min)

python
import redis
r = redis.Redis.from_url("redis://localhost:6379/0")
def is_duplicate_event(event_id: str) -> bool:
"""Retorna True se o evento já foi processado."""
key = f"wh:{event_id}"
# SET retorna None se a chave já existia
result = r.set(key, "1", ex=3600, nx=True)
return result is None

Passo 4 — Adicionar no início do handler (5 min)

python
def meu_webhook_handler(request):
payload = json.loads(request.body)
event_id = payload.get("id")
if event_id and is_duplicate_event(event_id):
return JsonResponse({"status": "ok"}, status=200) # ignora silenciosamente
# ... resto do código existente

Passo 5 — Testar com evento duplicado (8 min)

bash
# Simule reenvio duplicado
curl -X POST http://localhost:8000/webhook \
-H "Content-Type: application/json" \
-d '{"id": "evt_test_123", "type": "payment.completed"}'
# Mesma requisição — deve retornar ok sem processar
curl -X POST http://localhost:8000/webhook \
-H "Content-Type: application/json" \
-d '{"id": "evt_test_123", "type": "payment.completed"}'

Se o segundo request não disparou lógica de negócio, você está protegido.

O que acontece quando você ignora isso em escala

Durante o CaixaHub, processei webhooks de eventos bancários do Open Finance. Em dias de instabilidade — que são comuns no sistema financeiro brasileiro — a taxa de eventos duplicados chegava a 15%. Sem idempotência, 15% das transações seriam processadas em duplicata.

Em um sistema de pagamentos, isso não é bug de UX. É fraude acidental.

O padrão que funcionou: receiver síncrono respondendo em <100ms, Celery worker com deduplicação Redis + constraint único no banco, retry com backoff de 1s → 2s → 4s → 8s, e um dashboard simples mostrando a taxa de eventos duplicados por hora.

O dashboard foi o mais valioso. Picos na taxa de duplicatas indicavam problema no provider ou na rede — não no meu código. Virou sinal de alerta antecipado pra instabilidades externas.

Idempotência não é feature — é requisito

Sistemas que processam eventos externos sem idempotência estão funcionando por sorte. O provider não reenviar não significa que nunca vai reenviar.

Quando você adiciona IA ao pipeline, a aposta fica mais alta. Custo por token, efeitos colaterais assimétricos, latência que provoca timeout — tudo conspira pra transformar a ausência de idempotência em problema de produção em semanas, não meses.

A boa notícia: o padrão é estabelecido, o código é pequeno, e você pode implementar hoje. O Redis SET com NX é atômico. O UNIQUE constraint no banco é gratuito. O receiver assíncrono desacopla velocidade de resposta de complexidade de processamento.

Você não precisa de uma biblioteca especial. Precisa de disciplina arquitetural.

Se você está construindo automações com IA e quer mapear onde sua arquitetura atual tem pontos cegos de produção, o diagnóstico do site pode ajudar a identificar isso rapidamente: levilael.com.br/diagnosis

Compartilhar