Pular para o conteúdo
Categoria: PIX & Pagamentos Instantaneos5 min de leitura

Idempotência no PIX: o erro de arquitetura que duplica pagamento

Por Redacao Nixen ·

Todo sistema de pagamento que não trata idempotência vai pagar duas vezes — é questão de volume, não de sorte. O culpado quase nunca é o PIX: é o retry sobre um timeout que na verdade tinha dado certo.

O bug que só aparece quando dá certo

O relato é sempre parecido. O sistema roda meses sem incidente. Aí vem um pico — Black Friday, fechamento de mês, campanha — e aparecem pagamentos duplicados. Mesma transação, dois débitos, dois comprovantes.

O time olha o log e não encontra bug de lógica. O código está certo. E está mesmo.

O que aconteceu foi isto:

`` Aplicação ──── POST /pix ────▶ PSP │ ├── processa ├── DÉBITA A CONTA ✓ │ └── resposta ✗ (timeout de rede) Aplicação ← ← ← ← ← ← ← ← ← ← ← ← ← │ │ "não recebi resposta, então falhou" │ └──── POST /pix (retry) ───▶ PSP └── DÉBITA DE NOVO ✓ ``

O timeout não disse "falhou". Ele disse "não sei". E tratar "não sei" como "falhou" é o que duplica o pagamento.

Isso é especialmente cruel no PIX porque a liquidação é imediata e irrevogável. Não existe janela de cancelamento antes da compensação, como em boleto ou em cartão pré-autorizado. Debitou, saiu. A correção vira devolução — outra transação, com outra reconciliação, outro atrito com o cliente.

---

As três respostas possíveis, e o erro de tratar duas como uma

Toda chamada a um sistema de pagamento tem três desfechos:

| Resposta | Significado | O que fazer | |---|---|---| | 2xx | sucesso confirmado | seguir | | 4xx | falha determinística (dado inválido, saldo insuficiente) | não repetir — vai falhar igual | | timeout / 5xx / conexão caiu | estado desconhecido | consultar antes de repetir |

O bug nasce quando a terceira linha é implementada como se fosse a segunda. Retry cego sobre estado desconhecido é a definição do problema.

---

Idempotência: a mesma chamada, o mesmo efeito

Uma operação é idempotente quando executá-la N vezes produz o mesmo efeito de executá-la uma vez. Você consegue isso com uma chave de idempotência: um identificador que o cliente gera, envia junto e repete em todas as tentativas da mesma intenção de pagamento.

``` POST /pagamentos Idempotency-Key: 7f3a9c2e-...

1ª chamada → processa, guarda (chave → resultado), devolve 201 2ª chamada → encontra a chave, NÃO processa, devolve o MESMO resultado ```

Três detalhes decidem se isso funciona ou vira teatro:

A chave é da intenção, não da requisição. Ela precisa ser gerada uma vez, quando o usuário aperta "pagar", e sobreviver a todas as tentativas. Se você gerar um UUID novo a cada retry, tem chave de idempotência e não tem idempotência.

O registro da chave e o efeito precisam ser atômicos. Se você grava a chave numa transação e debita em outra, existe uma janela em que o processo morre no meio e você fica com o pior dos dois mundos. Uma transação só, ou nenhuma garantia.

A chave precisa persistir mais que a janela de retry. Guardar em memória de processo não resolve: o retry pode cair em outra instância. Precisa ser armazenamento compartilhado, com TTL maior que o maior retry possível — 24h é um ponto de partida razoável.

---

O que fazer quando o estado é desconhecido

Idempotência protege a escrita. Falta decidir o que fazer com a incerteza. A regra:

Diante de estado desconhecido, consulte antes de repetir.

`` timeout │ ▼ consultar por end-to-end ID / chave de idempotência │ ├── existe e liquidou → registrar como sucesso, não repetir ├── existe e falhou → pode repetir com NOVA intenção └── não existe → repetir com a MESMA chave ``

Isso pressupõe que você guarde o identificador antes de chamar o PSP. Guardar depois da resposta é o mesmo erro de novo: se a resposta não chega, você não tem por onde consultar.

Ordem correta:

```

  1. gerar e PERSISTIR a intenção (status: pendente, com chave)
  2. chamar o PSP
  3. atualizar a intenção com o resultado

```

Nunca 2 → 1.

---

Retry sem backoff é ataque ao seu próprio provedor

Repetir imediatamente, em loop, no momento em que o PSP está degradado, é adicionar carga a um sistema que já está sofrendo — e a sua fila cresce junto.

O mínimo aceitável: backoff exponencial com jitter e teto de tentativas.

`` tentativa 1 → espera ~1s tentativa 2 → espera ~2s tentativa 3 → espera ~4s tentativa 4 → espera ~8s depois disso → fila de conciliação, intervenção humana ``

O jitter (variação aleatória) evita que mil clientes que falharam juntos voltem exatamente juntos. Sem ele, você constrói uma onda que derruba o provedor no momento em que ele tenta se recuperar.

E retry infinito não existe. Depois do teto, a operação vai para conciliação — não fica girando para sempre.

---

Webhook não substitui conciliação

Muita implementação assume que o webhook do PSP é a fonte da verdade. Webhook se perde, chega fora de ordem, chega duplicado e às vezes chega antes de a sua própria transação ter commitado.

Por isso:

  • Trate o webhook como idempotente também. Mesmo evento pode chegar três vezes.
  • Não confie na ordem. O evento de liquidação pode chegar antes do de criação.
  • Tenha conciliação ativa. Um processo que, periodicamente, lista as transações pendentes há mais de X minutos e consulta o estado real no PSP. É esse processo que fecha o buraco quando o webhook falha — e ele vai falhar.

---

Checklist

  • [ ] Chave de idempotência gerada na intenção, não na requisição
  • [ ] Chave persistida antes da chamada ao PSP
  • [ ] Registro da chave e efeito na mesma transação
  • [ ] Armazenamento compartilhado, TTL > maior janela de retry
  • [ ] Timeout tratado como desconhecido, nunca como falha
  • [ ] Consulta de estado antes de qualquer repetição
  • [ ] Backoff exponencial com jitter e teto de tentativas
  • [ ] Webhook idempotente e tolerante a fora-de-ordem
  • [ ] Conciliação ativa para pendentes
  • [ ] Alerta para transações presas em pendente

Nenhum item é exótico. É que cada um deles só cobra a conta sob concorrência — e sob concorrência é sempre com dinheiro de cliente.

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly