Pular para o conteúdo
16 min de leitura

O que é idempotência: a propriedade que torna sistemas distribuídos confiáveis

Por Lucas Andrade ·

Idempotência é a garantia de que repetir uma operação não muda o resultado além da primeira vez — e é o que impede APIs e filas de cobrar ou processar em duplicidade.

Um cliente envia um pagamento. A requisição chega ao servidor, o cartão é cobrado, o banco de dados grava a transação — e então, antes da resposta voltar, a conexão cai. O cliente não sabe se o pagamento aconteceu. Nenhum humano no meio desse processo tomou uma decisão errada; a rede simplesmente fez o que redes fazem: perdeu um pacote, expirou um timeout, engasgou por um segundo. A pergunta que decide se esse sistema é confiável ou perigoso não é "a rede vai falhar?" — ela vai, sempre — mas "o que acontece quando o cliente, sem saber se deu certo, tenta de novo?".

A resposta correta para essa pergunta tem nome: idempotência. É uma das propriedades mais citadas e menos compreendidas em profundidade na construção de sistemas distribuídos, frequentemente reduzida a "pode chamar de novo que não tem problema", o que é verdadeiro mas incompleto. Entender exatamente o que a propriedade garante, onde ela se aplica e como construí-la deliberadamente é o que separa uma API que sobrevive a retries de uma que cobra duas vezes.

Definição precisa: repetir não muda o resultado

Idempotência é uma propriedade sobre efeito, não sobre resposta. Formalmente, uma operação é idempotente quando aplicá-la múltiplas vezes produz o mesmo estado final que aplicá-la uma única vez — a notação clássica é f(f(x)) = f(x). O ponto que costuma escapar é que essa igualdade fala do estado do sistema depois da operação, não do valor que a operação devolve a cada chamada. Uma rota pode devolver um corpo de resposta diferente a cada tentativa — um contador de tentativas, um timestamp de "última verificação" — e ainda ser idempotente, desde que o efeito colateral relevante para o domínio (o saldo, a linha no banco, o pedido criado) seja idêntico depois de uma chamada e depois de dez.

A propriedade é sobre o efeito líquido, avaliado do lado de fora do sistema. Não importa se, internamente, a segunda chamada disparou uma query a mais ou regravou uma linha com os mesmos valores — o que importa é que o mundo observável não mudou entre a primeira execução bem-sucedida e qualquer repetição subsequente. É essa garantia que permite a um cliente, ou a um proxy, ou a uma fila, reenviar uma operação sem precisar saber se a tentativa anterior teve sucesso, falhou, ou ficou em algum limbo intermediário — que é exatamente a situação mais comum em sistemas distribuídos, onde a confirmação de sucesso é, ela mesma, uma mensagem que pode se perder.

Idempotente não é o mesmo que "sem efeito colateral"

Confundir idempotência com ausência de efeito colateral é o erro conceitual mais comum do tema. Uma operação "safe" (seguro, no vocabulário da especificação HTTP) é aquela que não muda estado nenhum — uma leitura pura. Uma operação idempotente pode, e frequentemente deve, mudar estado; a garantia não é "nada muda", é "depois da primeira mudança, repetir não muda de novo". São conceitos relacionados mas distintos, e a hierarquia entre eles importa: toda operação safe é idempotente por definição — ler duas vezes o mesmo recurso, sem escrevê-lo, sempre devolve efeito líquido zero —, mas o inverso não vale. Uma operação idempotente pode ter escrito no banco, disparado uma auditoria, atualizado um índice; ela só não pode escrever de novo algo diferente do que já escreveu.

O exemplo mais direto é DELETE. Apagar um recurso muda o estado do sistema — claramente não é "sem efeito colateral" — mas é idempotente: apagar algo que já foi apagado deixa o sistema exatamente no mesmo estado que apagá-lo pela primeira vez. A primeira chamada devolve "removido"; a segunda pode devolver "não encontrado", uma resposta diferente, mas o estado do recurso — inexistente — é idêntico nas duas. É esse raciocínio, aplicado consistentemente, que separa os métodos HTTP em categorias que valem a pena memorizar.

Os métodos HTTP sob a lente da idempotência

| Método | Safe (sem efeito colateral) | Idempotente | Efeito repetido | |---|---|---|---| | GET | Sim | Sim | Nenhum — é leitura | | HEAD | Sim | Sim | Nenhum — é leitura | | PUT | Não | Sim | Substitui pelo mesmo valor — estado final igual | | DELETE | Não | Sim | Remove; repetir encontra "já removido" | | POST | Não | Não, por padrão | Cada chamada pode criar um novo recurso | | PATCH | Não | Depende de como é implementado | Idempotente se aplica um estado, não uma operação relativa |

PUT é idempotente porque substitui, não incrementa. PUT /usuarios/42 {"nome": "Ana"} diz "o estado do usuário 42 deve ser este"; enviar o mesmo corpo dez vezes deixa o usuário exatamente igual nas dez. POST /pedidos {"item": "x"}, por outro lado, tipicamente significa "crie um novo recurso com estes dados" — cada chamada, por padrão, cria um pedido novo, o que é o oposto de idempotência. É justamente por isso que POST é o método perigoso em cenários de retry: reenviar um POST que já teve sucesso do lado do servidor, mas cuja resposta se perdeu, cria um segundo pedido, um segundo cobrança, um segundo e-mail. PATCH fica no meio porque a especificação não dita idempotência: um PATCH que descreve o estado final desejado é idempotente; um PATCH que descreve uma operação relativa — "incremente o estoque em 5" — não é, e é fácil escrever um sem perceber que ele se comporta como o outro.

Por que isso importa: a rede mente

O motivo de idempotência não ser um detalhe acadêmico é que a premissa por trás dela — a rede falha e o cliente reenvia — não é hipotética, é o comportamento padrão de qualquer sistema em produção. Timeouts expiram mesmo quando o servidor terminou o trabalho a tempo; conexões caem entre o servidor processar a requisição e a resposta chegar de volta; load balancers reenviam requisições que consideram travadas; bibliotecas de HTTP client vêm com retry automático embutido, às vezes ligado por padrão sem o desenvolvedor perceber. Em cada um desses casos, o cliente enfrenta uma ambiguidade fundamental que nenhuma engenharia de rede resolve por completo: um timeout não diz "a operação falhou", diz apenas "eu não sei se a operação teve sucesso".

Essa ambiguidade tem três desfechos possíveis, e só um deles é seguro assumir. A requisição pode não ter chegado ao servidor, pode ter chegado e falhado, ou pode ter chegado e tido sucesso — com a resposta se perdendo no caminho de volta. Um cliente que trata timeout como "certamente falhou" e reenvia está correto nos dois primeiros casos e desastrado no terceiro. Como não há como o cliente distinguir os três casos de fora, a única saída sólida é fazer com que o terceiro caso — reenviar uma operação que já teve sucesso — seja inofensivo. Isso é, por definição, o que idempotência garante.

O problema de cobrar duas vezes

O exemplo mais visceral é o de pagamento, porque o efeito colateral é dinheiro saindo da conta de alguém. Um app de checkout envia POST /pagamentos, o gateway processa a cobrança, debita o cartão, e a resposta de sucesso se perde a caminho do app por um problema de rede. O app, vendo um timeout, não sabe se o pagamento aconteceu — e a reação natural do usuário, "não deu certo, vou tentar de novo", dispara um segundo POST, com os mesmos dados, para uma rota que por padrão trata cada chamada como um evento de cobrança novo. Sem nenhuma proteção adicional, o resultado é uma cobrança duplicada real, visível no extrato, gerando um chamado de suporte, uma reversão manual e uma perda de confiança que nenhuma mensagem de erro bem escrita compensa depois do fato.

O erro não está em o cliente reenviar — reenviar sob incerteza é o comportamento correto e esperado. O erro está em o servidor não ter como saber que aquele segundo POST é uma repetição da mesma intenção, e não uma nova intenção. É exatamente essa lacuna que a idempotência, aplicada deliberadamente a operações que por natureza não são idempotentes, fecha.

Idempotency keys: tornando o POST seguro

Como POST não é idempotente por padrão, e ainda assim é o método natural para "criar" e "executar", a solução da indústria é adicionar idempotência por fora do método: a chave de idempotência. O cliente gera um identificador único — em geral um UUID — para a intenção da operação, não para cada tentativa de rede, e o envia em um cabeçalho, tipicamente Idempotency-Key. O servidor memoriza, por um período de retenção razoável, o resultado da primeira execução associada a essa chave. Se a mesma chave chegar de novo, o servidor não executa a operação uma segunda vez: devolve a resposta que já tinha guardado da primeira.

``` POST /v1/pagamentos HTTP/1.1 Idempotency-Key: 8f14e45f-ceea-467e-bd42-... Content-Type: application/json

{"valor_centavos": 15000, "cliente_id": "cus_9x2k"} ```

No servidor, o fluxo conceitual é simples de descrever e delicado de implementar corretamente sob concorrência:

```sql -- tabela que memoriza a chave e o resultado CREATE TABLE idempotency_keys ( key TEXT PRIMARY KEY, status TEXT NOT NULL, -- 'processando' | 'concluido' response_body JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );

-- ao receber a requisição: INSERT INTO idempotency_keys (key, status) VALUES ($1, 'processando') ON CONFLICT (key) DO NOTHING; -- se 0 linhas afetadas: a chave já existe — devolve o response_body salvo -- se 1 linha afetada: esta é a primeira vez — processa o pagamento e -- depois faz UPDATE gravando status='concluido' e o response_body ```

O detalhe que faz essa técnica funcionar sob concorrência real é o INSERT ... ON CONFLICT DO NOTHING combinado com checar quantas linhas foram afetadas — não um SELECT seguido de INSERT em passos separados. Duas requisições com a mesma chave podem chegar quase simultaneamente, por exemplo um cliente que reenviou por timeout enquanto a primeira tentativa ainda estava em voo; se a verificação de existência e a inserção são operações separadas, ambas podem "ver" a chave como inexistente e as duas processam o pagamento. A operação atômica de inserção condicional é o que garante que, mesmo sob corrida, só uma execução vence. A mesma chave, importante frisar, deve corresponder ao mesmo corpo de requisição; um servidor cuidadoso rejeita ou trata como erro uma chave reutilizada com um payload diferente, porque isso normalmente indica um bug do cliente, não uma repetição legítima.

Filas e mensageria: at-least-once exige consumidor idempotente

O mesmo raciocínio de "a confirmação pode se perder" se aplica, com ainda mais força, a sistemas de mensageria. A maioria das filas em produção — Kafka, SQS, RabbitMQ, NATS com JetStream — opera sob uma garantia chamada at-least-once: a mensagem é entregue ao menos uma vez, o que na prática significa que ela pode ser entregue mais de uma vez. Isso não é um defeito da fila, é uma escolha de design deliberada, porque a alternativa — garantir at-most-once, entregar no máximo uma vez — corre o risco oposto e pior de simplesmente perder a mensagem se o consumidor cair no meio do processamento.

O mecanismo por trás do at-least-once é o próprio acknowledgment. O consumidor lê a mensagem, processa, e só então confirma ("ack") que terminou; a fila, por sua vez, só remove a mensagem da fila de pendências depois do ack. Se o consumidor processa a mensagem inteira e cai — ou perde a conexão — exatamente entre terminar o processamento e mandar o ack, a fila nunca recebe a confirmação e, depois de um tempo, reentrega a mesma mensagem para outro consumidor (ou para o mesmo, ao voltar). Do ponto de vista da fila, isso é o comportamento correto: ela não tem como saber que o trabalho já foi feito, só sabe que não foi confirmado. A obrigação de lidar com essa reentrega cai inteiramente sobre quem consome.

Isso significa que todo consumidor de fila em produção precisa ser escrito assumindo que vai processar a mesma mensagem mais de uma vez, eventualmente. As táticas para isso são as mesmas da idempotency key, adaptadas ao contexto: deduplicação por um identificador que viaja na mensagem (guardar os IDs já processados e ignorar repetições), UPSERT em vez de INSERT puro no efeito final, ou desenhar a operação de forma que seja naturalmente comutativa e idempotente — "definir o status como pago" em vez de "somar um pagamento ao total". Um consumidor que assume ingenuamente exactly-once — que cada mensagem chega uma única vez — está escrevendo um bug que só aparece sob carga, quando reentregas deixam de ser um evento raro de laboratório e passam a acontecer todo dia.

Como tornar uma operação idempotente na prática

A técnica mais recorrente é trocar INSERT por UPSERT. Um INSERT puro, executado duas vezes com a mesma chave primária, falha na segunda vez com violação de unicidade — o que, ironicamente, ao menos avisa que algo se repetiu, mas ainda quebra o fluxo se não for tratado. Um UPSERT (INSERT ... ON CONFLICT ... DO UPDATE, ou o equivalente MERGE em outros bancos) resolve isso de forma natural: a primeira execução cria a linha, qualquer execução seguinte com a mesma chave grava exatamente os mesmos valores por cima, e o estado final é indistinguível entre ter rodado uma vez ou dez.

``sql INSERT INTO assinaturas (usuario_id, plano, ativa_desde) VALUES ($1, $2, $3) ON CONFLICT (usuario_id) DO UPDATE SET plano = EXCLUDED.plano, ativa_desde = EXCLUDED.ativa_desde; ``

Outra tática é checar o estado antes de agir, em vez de assumir uma transição. Em vez de "envie a notificação", a operação vira "envie a notificação se o campo notificado_em ainda estiver nulo, e marque o campo ao enviar" — o que transforma uma ação irrestrita em uma transição de estado guardada, que só acontece uma vez mesmo sob reentrega. Uma terceira tática, útil quando o domínio permite, é usar uma chave natural em vez de gerar um novo identificador a cada chamada: se o pedido de um cliente já tem um número de referência vindo do sistema de origem, usar esse número como chave primária torna o INSERT da linha de pedido idempotente de graça, sem precisar de nenhuma tabela auxiliar de deduplicação.

Armadilhas comuns

Incrementar um contador nunca é idempotente, mesmo que pareça inofensivo. UPDATE estoque SET quantidade = quantidade + 1 muda o resultado a cada execução por definição — é uma operação relativa ao estado atual, não uma atribuição de estado final. Rodá-la duas vezes por causa de um retry decrementa o estoque errado silenciosamente, sem erro nenhum para alertar ninguém. A correção é sempre a mesma: transformar a operação relativa em uma atribuição guardada por uma chave — gravar o evento "estoque decrementado pelo pedido X" com X como chave única, e derivar a quantidade total somando eventos distintos, em vez de mutar um contador direto.

Efeitos colaterais externos ao seu banco de dados são a parte mais difícil de tornar idempotente, porque frequentemente não há como desfazê-los. Enviar um e-mail, disparar um SMS, cobrar um cartão em um gateway de terceiros — nesses casos, "repetir sem mudar o resultado" não pode significar "processar de novo e sobrescrever", porque o efeito já saiu do seu controle no instante em que aconteceu. A saída aqui não é tornar o envio em si idempotente, é impedir que ele aconteça duas vezes: registrar, antes de disparar, que o envio para aquela chave está em andamento, e checar esse registro antes de qualquer tentativa subsequente — na prática, a mesma tabela de idempotency keys, aplicada não à resposta HTTP mas à chamada ao serviço externo.

Assumir que o cliente vai se comportar direito é outra armadilha. Um cliente malicioso, ou só malfeito, pode gerar uma chave de idempotência nova a cada tentativa — derrotando o propósito inteiro do mecanismo. Idempotency keys protegem contra reenvio honesto sob incerteza de rede; não substituem validação de negócio, limites de taxa, nem a checagem de que a mesma operação, com a mesma chave, não está sendo usada para tentar burlar um limite de tentativas.

Idempotência não é exactly-once (mas é o que existe de fato)

Vale desfazer, para fechar, uma confusão de vocabulário que aparece com frequência: "exactly-once" — processar exatamente uma vez, nem mais nem menos — soa como a garantia ideal, mas não existe de forma pura em sistemas distribuídos reais, e entender por quê é instrutivo. Garantir exactly-once de ponta a ponta exigiria que o produtor soubesse, com certeza absoluta, que sua mensagem foi recebida e processada — o que de novo esbarra no mesmo problema fundamental: a confirmação dessa certeza é, ela mesma, uma mensagem sujeita a se perder. É ambiguidade de rede recursiva, sem saída limpa.

**O que existe, e que na prática entrega o mesmo resultado observável que exactly-once, é a combinação de at-least-once com idempotência — às vezes chamada de effectively-once.** A infraestrutura garante que a mensagem chega ao menos uma vez, nunca menos; o consumidor garante que processá-la mais de uma vez não muda o efeito líquido. O resultado, de fora, é indistinguível de ter processado exatamente uma vez — mas chegou lá por um caminho diferente e mais robusto: não impedindo a duplicata, e sim tornando a duplicata inofensiva. É uma mudança de postura que vale generalizar para todo o resto do sistema: em vez de perseguir a garantia impossível de que nada nunca se repete, desenhar cada operação para que repetição, quando acontecer — e ela vai acontecer —, seja um não-evento.

Fechando

Idempotência não é um truque de implementação, é uma propriedade de design que reconhece uma verdade desconfortável sobre redes: elas falham, confirmações se perdem, e o cliente que reenvia sob incerteza está se comportando corretamente. A diferença entre um sistema frágil e um confiável não é evitar essas falhas — é impossível evitá-las — é garantir que repetir uma operação, de propósito ou por acidente, produza o mesmo estado final que executá-la uma única vez. GET e HEAD ganham essa garantia de graça por serem seguros; PUT e DELETE a ganham por definição de método; POST precisa dela desenhada explicitamente, normalmente via idempotency key. Todo consumidor de fila precisa assumir at-least-once e responder com deduplicação, upsert ou transições de estado guardadas — nunca com contadores incrementais nem envios irrestritos de efeitos externos. Não existe exactly-once de verdade; existe at-least-once mais idempotência, e essa combinação, bem aplicada, é o que faz um retry ser uma rede de segurança em vez de um segundo débito no cartão de alguém.

Leituras relacionadas

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