Pular para o conteúdo

Certificação Claude Developer Foundations · parte 4 de 20

Idempotência: por que repetir uma ação pode causar dano

Por que repetir uma chamada pode causar dano em pagamentos, filas e chamadas a modelos, e como chaves de idempotência e restrições evitam isso.

Publicada em · 16 min de leitura

Tamanho da fonte:
Largura da coluna:

Idempotência é a propriedade de uma operação que pode ser executada múltiplas vezes sem alterar o resultado além da primeira aplicação.

Ou seja: $f(x) = f(f(x))$. Não importa se você aperta o botão uma ou cem vezes; o estado final do sistema deve ser o mesmo que o da primeira execução bem-sucedida.

🛠️ O Exemplo do Mundo Real: O Elevador

Imagine que você está no térreo e quer ir para o 10º andar.

  1. Você aperta o botão “10”. O elevador entende o comando e começa a subir.

  2. Se você apertar o botão “10” mais cinco vezes enquanto ele sobe, o elevador não vai parar no 50º andar e nem vai tentar “re-subir”. Ele simplesmente ignora os cliques extras porque o objetivo final (estar no 10º andar) já foi definido.

Isso é ser idempotente.


💻 No Contexto de Desenvolvimento (HTTP & APIs)

Se você trabalha com REST, já usa (ou deveria usar) esse conceito diariamente.

1. Métodos HTTP Idempotentes

  • GET: Pedir os dados de um usuário dez vezes não deve alterar o usuário.

  • PUT: Atualizar o e-mail do usuário para joao@email.com. Se você enviar a mesma requisição 10 vezes, o e-mail continuará sendo o mesmo.

  • DELETE: Deletar o registro ID 5. Na primeira vez, ele é removido. Nas próximas, ele continua “não existindo” (o resultado final de ausência é o mesmo).

2. Método NÃO Idempotente

  • POST: Imagine uma rota /compras/finalizar. Se o usuário clicar no botão “Comprar”, a conexão oscilar e ele clicar de novo, sem um tratamento de idempotência, você pode cobrar o cartão dele duas vezes e gerar dois pedidos.

🚀 Casos de Uso e Como Implementar

Como um Dev Senior, você não apenas sabe o conceito, mas sabe onde ele salva sua pele:

A. Processamento de Pagamentos

Este é o caso clássico. Para evitar cobranças duplicadas em caso de instabilidade de rede, usamos uma Idempotency Key (geralmente um UUID enviado no Header).

  • O fluxo: O backend recebe a chave, verifica no banco/cache se já processou aquela chave nos últimos X minutos. Se sim, ele retorna a resposta da primeira vez sem processar o pagamento novamente.

B. Consumo de Mensagens (Filas/RabbitMQ/Kafka)

Sistemas de mensageria garantem a entrega, mas às vezes entregam a mesma mensagem duas vezes (at-least-once delivery).

  • Solução: Seu Consumer deve verificar se o ID daquela mensagem já foi processado antes de realizar uma operação de escrita no banco de dados.

C. Scripts de Infraestrutura (Infrastructure as Code)

Ferramentas como Ansible ou Terraform são baseadas em idempotência. Se você rodar um script para “Instalar o Docker”, e o Docker já estiver lá, o script não faz nada. Ele apenas garante o estado desejado.


💡 Dica de Ouro: O “Safeguard”

Ao projetar APIs, sempre pergunte: “O que acontece se o cliente mandar isso duas vezes por erro de rede?”. Se a resposta for “vai dar erro de integridade” ou “vai duplicar dado”, você precisa de uma estratégia de idempotência.

Uma forma simples de implementar é usar o Banco de Dados a seu favor: Unique Constraints em campos combinados ou uma tabela de processed_requests resolvem 90% dos problemas.

Em Go, a idempotência é fundamental, especialmente porque a linguagem é muito usada em sistemas distribuídos e microsserviços, onde falhas de rede e retentativas (retries) são comuns.

Vou te mostrar um padrão de implementação focado em uma API de pagamentos, que é o cenário onde a falta de idempotência mais causa prejuízo.


🏗️ O Padrão: Tabela de Chaves de Idempotência

A estratégia mais robusta é usar um Middleware ou uma verificação no início da função que consulta uma tabela de “chaves processadas”.

1. O Schema do Banco (PostgreSQL)

Precisamos de uma tabela para registrar as requisições que já foram “vistas”.

CREATE TABLE idempotency_keys (
    idempotency_key UUID PRIMARY KEY,
    response_code INT NOT NULL,
    response_body JSONB NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

2. Implementação em Go

Aqui está um exemplo de como estruturar essa lógica no seu serviço. Imagine que estamos usando o driver pgx para o banco de dados.

package main

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"time"

	"github.com/jackc/pgx/v5"
)

type PaymentRequest struct {
	Amount   int    `json:"amount"`
	Currency string `json:"currency"`
}

type PaymentResponse struct {
	TransactionID string `json:"transaction_id"`
	Status        string `json:"status"`
}

// ProcessPayment lida com a lógica de negócio
func ProcessPayment(ctx context.Context, db *pgx.Conn, idempKey string, req PaymentRequest) (int, any, error) {
	// 1. Iniciar uma Transação
	tx, err := db.Begin(ctx)
	if err != nil {
		return http.StatusInternalServerError, nil, err
	}
	defer tx.Rollback(ctx)

	// 2. Verificar se a chave de idempotência já existe
	var savedCode int
	var savedBody []byte
	err = tx.QueryRow(ctx, 
		"SELECT response_code, response_body FROM idempotency_keys WHERE idempotency_key = $1", 
		idempKey).Scan(&savedCode, &savedBody)

	if err == nil {
		// Chave encontrada! Retornamos a mesma resposta de antes sem reprocessar
		var responseData any
		json.Unmarshal(savedBody, &responseData)
		return savedCode, responseData, nil
	}

	if !errors.Is(err, pgx.ErrNoRows) {
		return http.StatusInternalServerError, nil, err
	}

	// 3. Lógica de Negócio Real (Simulada)
	// Aqui você chamaria o gateway de pagamento (Stripe, etc)
	result := PaymentResponse{
		TransactionID: "tx_12345ABC",
		Status:        "success",
	}
	
	resultJSON, _ := json.Marshal(result)
	statusCode := http.StatusCreated

	// 4. Salvar o resultado na tabela de idempotência ANTES do commit
	_, err = tx.Exec(ctx, 
		"INSERT INTO idempotency_keys (idempotency_key, response_code, response_body) VALUES ($1, $2, $3)",
		idempKey, statusCode, resultJSON)
	
	if err != nil {
		return http.StatusInternalServerError, nil, err
	}

	// 5. Commit Final
	if err := tx.Commit(ctx); err != nil {
		return http.StatusInternalServerError, nil, err
	}

	return statusCode, result, nil
}

🔍 Por que essa abordagem é “Senior”?

  1. Atomicidade (Transação): O registro da chave de idempotência e a operação de negócio (ou o registro dela) acontecem na mesma transação. Se um falhar, nada é salvo.

  2. Cache de Resposta: Note que salvamos o response_body. Isso é importante porque se o cliente enviar a mesma chave, ele não quer apenas que o sistema não duplique; ele quer receber exatamente a mesma resposta (ex: o TransactionID) que teria recebido na primeira vez.

  3. Segurança de Concorrência: Se duas requisições idênticas chegarem ao mesmo milissegundo, a PRIMARY KEY (ou um UNIQUE constraint) do banco de dados impedirá que as duas sejam processadas. Uma delas vai dar erro de inserção, e você pode tratar isso.

⚠️ Cuidados em Produção

  • Expiração: Chaves de idempotência não devem ser eternas. Geralmente, define-se um TTL (Time To Live) de 24h a 7 dias. Você pode ter um worker em Go que limpa essa tabela periodicamente.

  • Escopo: A chave de idempotência deve ser única por cliente. Caso contrário, o Cliente A pode mandar o UUID 123 e impedir o Cliente B de mandar o mesmo UUID.


Retentativas em chamadas a modelos

Tudo o que a aula mostrou até aqui vale para qualquer API, mas há um caso que ganhou peso nos últimos anos: chamar um modelo de linguagem pela rede. Uma chamada à API do Claude é uma requisição HTTP como outra qualquer, e como qualquer requisição ela falha por motivos que não têm a ver com o seu código: o provedor está sobrecarregado, você passou do limite de uso, a conexão caiu no meio. A pergunta prática é quando vale tentar de novo e quando tentar de novo só piora a situação. O raciocínio é o mesmo de um pagamento ou de uma fila: repetir só é seguro quando a repetição não causa dano.

A regra de ouro é separar erro transitório de erro permanente. Um erro transitório é aquele que pode sumir sozinho se você esperar um pouco: o mesmo pedido, enviado de novo, tem chance real de dar certo. Um erro permanente é aquele em que o pedido, do jeito que está, nunca vai funcionar, então repeti-lo só gasta tempo e cota. A documentação de erros da Anthropic lista os códigos que a API devolve, e dá para sortear cada um num dos dois grupos.

Entre os transitórios estão o 429 (rate_limit_error), quando a organização bateu um limite de taxa; o 500 (api_error), um erro interno do lado da Anthropic, para o qual a própria documentação manda tentar de novo com espera exponencial; o 529 (overloaded_error), que indica sobrecarga temporária da API; e o 504 (timeout_error), em que a requisição estourou o tempo enquanto era processada. O 409 (conflict_error) descreve um conflito com o estado de um recurso, e a documentação manda resolver o conflito e então repetir. Erros de conexão, em que nem chega resposta, entram no mesmo grupo.

Entre os permanentes, o principal é o 400 (invalid_request_error): o formato ou o conteúdo do pedido está errado. Mandar o mesmo JSON malformado dez vezes dá dez vezes o mesmo 400. A documentação mostra exemplos concretos, como pedir um modo de pensamento que o modelo escolhido não aceita. Os códigos 401 (chave inválida), 402 (cobrança), 403 (sem permissão), 404 (recurso não existe) e 413 (requisição grande demais) pedem uma correção no seu lado, não paciência. A documentação descreve a causa de cada um, mas não escreve literalmente “não repita”; essa conclusão é minha, a partir da causa.

Há duas armadilhas que a documentação aponta e que enganam quem decide só pelo número do código. A primeira: o 429 nem sempre é transitório. Quando a organização atinge o teto mensal de gasto da sua faixa de uso, a API também responde 429 com rate_limit_error, mas sem cabeçalho retry-after, e o erro continua até o acesso voltar (no primeiro dia do mês seguinte, segundo a página de limites). O corpo traz error.details.error_code igual a enforced_spend_limit_reached, e é por esse campo que se distingue esse caso de um limite de taxa comum. A segunda: um limite de gasto que você mesmo configurou devolve 400, não 429. Em ambos os casos insistir não adianta.

Espera exponencial e o cabeçalho retry-after

Esperar um tempo fixo entre as tentativas é uma péssima ideia quando o problema é sobrecarga. Se mil clientes falham ao mesmo tempo e todos tentam de novo depois de um segundo, a API recebe mil requisições de uma vez, de novo. A espera exponencial resolve isso dobrando o intervalo a cada falha (0,5 s, 1 s, 2 s, 4 s e assim por diante, até um teto). Costuma-se somar um jitter, uma variação aleatória pequena, para que os clientes não voltem sincronizados. Sem o jitter, todos dobram juntos e continuam em fila.

O cabeçalho retry-after entra como uma instrução do servidor que vale mais que o seu cálculo. A página de limites de taxa o define como o número de segundos a esperar até poder tentar de novo, e avisa que tentativas anteriores a esse prazo vão falhar. Ela também diz que nem todo 429 o traz. A regra prática, então, é: se o cabeçalho existe, espere pelo menos esse valor; se não existe, use a espera exponencial. A mesma página explica que a API usa o algoritmo de balde de fichas (token bucket), em que a capacidade é reposta continuamente em vez de zerar num horário fixo, o que explica por que esperar um pouco costuma bastar. Ela ainda avisa que um aumento brusco de tráfego pode causar 429 por limites de aceleração, e a recomendação é subir o tráfego aos poucos.

Os SDKs oficiais já fazem isso por você. A documentação diz que eles repetem automaticamente falhas transitórias (erros de conexão, limites de taxa e erros 5xx) com espera exponencial, duas vezes por padrão, respeitando retry-after quando presente, e que o parâmetro max_retries (maxRetries em TypeScript) configura ou desliga o comportamento. O código-fonte do SDK Python, que li no GitHub em 18/10/2026, mostra os detalhes que a página de documentação não traz: atraso inicial de 0,5 s, teto de 8 s, jitter que reduz a espera em até 25%, e repetição também para 408 e 409, além de 429 e 5xx. Esses números vêm do código e não de uma página de documentação, então podem mudar entre versões. O mesmo código ainda respeita um cabeçalho não padronizado, x-should-retry, quando o servidor o envia. Isso importa para a prova e para o dia a dia: antes de escrever seu próprio laço, confira se o SDK já resolve, porque somar um laço seu por cima do dele multiplica as tentativas (três tentativas suas vezes três do SDK são nove chamadas).

Um caso especial é a resposta em streaming. A documentação de erros observa que, numa resposta por eventos enviados pelo servidor (SSE), um erro pode surgir depois de a API já ter devolvido 200. Nesse caso o tratamento padrão por código HTTP não se aplica, e o erro chega como evento no meio do fluxo. Quem repete uma chamada em streaming precisa decidir o que fazer com o texto parcial já recebido, e a documentação sobre eventos de erro descreve o formato.

O risco de repetir uma chamada com efeito colateral

Repetir uma consulta é inofensivo. Repetir uma ação que mexe no mundo não é. O ponto delicado das falhas de rede é que elas escondem em qual lado a coisa quebrou: um 504 ou uma conexão que cai depois do envio não diz se o servidor chegou a processar o pedido. Você só sabe que não recebeu a resposta. Se o pedido era “enviar e-mail ao cliente” ou “cobrar R$ 80”, tentar de novo pode executá-lo pela segunda vez. É exatamente o problema do elevador desta aula, só que aqui o botão é apertado por um laço de código, sem ninguém para perceber.

Com modelos, isso aparece de duas formas. Na primeira, a própria chamada ao modelo é repetida. Ela costuma ser segura do ponto de vista de efeitos externos, porque gerar texto não altera o seu sistema. Duas ressalvas: a resposta da repetição pode ser diferente da primeira (o modelo não é determinístico), então a operação é segura mas não produz sempre o mesmo resultado; e se uma chamada que estourou o tempo foi processada e cobrada no servidor, você pode pagar pelas duas. Esse segundo ponto eu não consegui confirmar nas páginas de erro e limites que consultei, então trate como uma dúvida a verificar na sua conta.

A segunda forma é a que pesa mais: o modelo pede uma ferramenta (uso de ferramentas, ou tool use) e o seu código a executa, por exemplo criar_pedido ou enviar_email. A repetição de qualquer etapa dessa cadeia, seja a chamada ao modelo que falhou ao devolver o resultado, seja o próprio laço do agente que reinicia, pode acionar a ferramenta de novo. A defesa está no código da ferramenta, não no modelo: ela precisa ser idempotente, com o mesmo padrão da tabela de chaves que a aula já mostrou para pagamentos.

Há uma decisão de projeto importante na escolha da chave. Ela deve vir da intenção de negócio (o identificador do pedido, o par cliente e tipo de aviso), e não de algo que muda a cada tentativa. Se a repetição da chamada ao modelo gera uma resposta nova, o identificador do bloco de uso de ferramenta nessa resposta também será novo, e uma chave baseada nele nunca reconheceria a duplicata. Essa conclusão é um raciocínio meu sobre o mecanismo, não uma recomendação da documentação.

Quanto ao suporte da própria API: nas páginas de erros, limites e visão geral da API Messages que consultei, não encontrei nenhuma menção a chave de idempotência, e no código do cliente Python da Anthropic não vi um cabeçalho de idempotência configurado (a base do SDK tem suporte, mas o cliente não define o nome do cabeçalho). Portanto, não conte com a API para desduplicar chamadas ao modelo. Se a documentação mais recente trouxer algo, vale conferir antes de decidir. A idempotência que você controla é a das ações que o seu sistema executa.

Quando não há uma tabela de chaves à mão, o estado da aplicação pode cumprir o papel. Antes de agir, consulte: o e-mail deste pedido já foi enviado? O pedido já existe? Para isso funcionar, a marca de “feito” precisa ser gravada junto do efeito, na mesma transação sempre que possível, como na ProcessPayment acima. Se o efeito acontece e a gravação falha, a próxima tentativa não enxerga a marca e repete a ação.

Um laço de retentativa que eu rodei

Para fixar as duas ideias, montei um script curto em Python, sem rede: um servidor falso devolve uma sequência combinada de erros, e o laço decide o que fazer. A espera real é dividida por 10 só para a demonstração não demorar, mas o valor impresso é o que seria esperado. O terceiro caso compara uma ação que falha depois de executar, com e sem chave.

import random
import time

RETRYABLE = {408, 409, 429, 500, 502, 503, 504, 529}


class ApiError(Exception):
    def __init__(self, status, retry_after=None):
        super().__init__(f"HTTP {status}")
        self.status = status
        self.retry_after = retry_after


def call_with_retry(fn, max_retries=4, base=0.5, cap=8.0):
    for attempt in range(max_retries + 1):
        try:
            return fn()
        except ApiError as e:
            if e.status not in RETRYABLE or attempt == max_retries:
                print(f"  tentativa {attempt + 1}: {e} -> desiste")
                raise
            espera = min(cap, base * 2 ** attempt) * (1 - 0.25 * random.random())
            if e.retry_after is not None:
                espera = max(espera, e.retry_after)
            print(f"  tentativa {attempt + 1}: {e} -> espera {espera:.2f}s")
            time.sleep(espera / 10)  # acelerado 10x para a demo


def servidor(respostas):
    it = iter(respostas)
    def chamada():
        r = next(it)
        if isinstance(r, ApiError):
            raise r
        return r
    return chamada


random.seed(1)
print("caso 1: 429 com retry-after, depois 529, depois sucesso")
print("  resultado:", call_with_retry(servidor([ApiError(429, retry_after=2), ApiError(529), "ok"])))
print("caso 2: 400 invalid_request_error")
try:
    call_with_retry(servidor([ApiError(400), "ok"]))
except ApiError:
    pass

print("caso 3: efeito colateral com chave de idempotência")
enviados, vistos = [], {}
def registrar(chave, texto, falha):
    # grava a chave junto do efeito, antes de a resposta poder se perder
    if chave in vistos:
        print(f"  chave {chave}: já executada, não envia de novo")
        return vistos[chave]
    enviados.append(texto)
    vistos[chave] = "enviado"
    if falha:
        raise ApiError(504)
    return vistos[chave]

estado = {"n": 0}
def passo():
    estado["n"] += 1
    return registrar("pedido-42", "e-mail ao cliente", falha=estado["n"] == 1)
call_with_retry(passo)
print("  e-mails enviados:", len(enviados))

enviados.clear()
estado["n"] = 0
def passo_ingenuo():
    estado["n"] += 1
    enviados.append("e-mail ao cliente")
    if estado["n"] == 1:
        raise ApiError(504)
call_with_retry(passo_ingenuo)
print("  sem chave, e-mails enviados:", len(enviados))

Saída real da execução:

caso 1: 429 com retry-after, depois 529, depois sucesso
  tentativa 1: HTTP 429 -> espera 2.00s
  tentativa 2: HTTP 529 -> espera 0.79s
  resultado: ok
caso 2: 400 invalid_request_error
  tentativa 1: HTTP 400 -> desiste
caso 3: efeito colateral com chave de idempotência
  tentativa 1: HTTP 504 -> espera 0.40s
  chave pedido-42: já executada, não envia de novo
  e-mails enviados: 1
  tentativa 1: HTTP 504 -> espera 0.47s
  sem chave, e-mails enviados: 2

No caso 1, o 429 trouxe retry-after de 2 s, e o laço esperou 2,00 s, em vez dos cerca de 0,5 s que a espera exponencial daria na primeira tentativa. Na segunda falha (529) não havia o cabeçalho, então valeu o cálculo próprio: 1 s menos o jitter, 0,79 s. No caso 2, o 400 nem chega a esperar: o laço desiste na primeira tentativa, porque repetir um pedido inválido não o conserta. No caso 3, o servidor “executou” a ação e perdeu a resposta com um 504. Com a chave, a segunda tentativa reconheceu o trabalho feito e o e-mail saiu uma vez. Sem a chave, o mesmo laço, com o mesmo erro, enviou dois e-mails. O laço é idêntico nos dois; o que muda é a ação ser idempotente ou não.

Dois limites desse exemplo. O servidor é simulado: ele não reproduz a latência, os limites nem os cabeçalhos reais da API, só a forma como o laço reage a eles. E o laço é didático: em produção, o certo é começar pelo max_retries do SDK oficial e só escrever lógica própria quando precisar de algo que ele não faz, como repetir só um subconjunto de erros ou registrar métricas de cada tentativa.

Fontes das seções sobre modelos

Consultadas em 18/10/2026.

Não verificado: se a API Messages aceita algum mecanismo de chave de idempotência (não achei nas páginas consultadas), e se uma chamada que estoura o tempo é cobrada quando o servidor chegou a processá-la.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?