Um timeout não indica que uma operação falhou: apenas confirma que o cliente não recebeu uma resposta dentro do prazo. O servidor pode ter criado o pedido, o provedor de pagamentos pode ter aceitado a cobrança ou um processo assíncrono pode continuar em execução. Se o cliente tentar novamente sem controle, uma mesma intenção de negócio pode produzir efeitos duplicados.
A idempotência em PHP transforma uma repetição técnica em uma consulta ou na devolução do resultado já obtido. Não consiste em ignorar todos os duplicados nem em confiar apenas que o usuário não clique duas vezes. É um contrato explícito entre cliente, API, persistência e, quando aplicável, sistemas externos.
O problema: a resposta se perde, mas o efeito permanece

Considere um endpoint que confirma uma compra. A aplicação valida a solicitação, registra o pedido, solicita a cobrança e prepara uma resposta. A conexão é interrompida pouco antes de o cliente recebê-la. Ao reenviar o mesmo formulário, o endpoint não pode deduzir pelo conteúdo que se trata da mesma compra: dois pedidos com os mesmos produtos podem ser intenções válidas e distintas.
O problema também surge em cadastros de usuários, atribuição de créditos, emissão de documentos, sincronizações, webhooks e ações administrativas. Há três elementos que convém separar:
- Intenção de negócio: «quero confirmar esta compra específica».
- Solicitação técnica: um envio HTTP com cabeçalhos, corpo e contexto de autenticação.
- Tentativa de execução: cada processamento interno, nova tentativa da fila ou chamada a um provedor.
A chave idempotente identifica a intenção, não uma conexão HTTP nem cada tentativa do servidor. Por isso, ela deve sobreviver a novas tentativas de rede e, quando o fluxo exigir, a reinicializações do processo.
Quais operações precisam de idempotência e quais não
Priorize operações que criam, confirmam, cobram, enviam, reservam, notificam ou modificam um recurso com consequências relevantes. Um POST /payments, a confirmação de um pedido ou o recebimento de um webhook são candidatos claros. O mesmo vale para um trabalho de fila que pode ser entregue mais de uma vez.
Uma leitura pura normalmente não precisa de uma chave de idempotência. Uma atualização pode ter uma semântica diferente: definir um estado desejado, como PUT /profiles/42, pode ser idempotente por design se a mesma representação deixar o recurso igual. Em contrapartida, uma ação como «adicionar saldo» não é idempotente apenas por usar um verbo determinado.
Também não se deve usar uma chave como substituta para outras regras. Para impedir duas reservas compatíveis em um inventário limitado, são necessárias invariantes de domínio, controle de concorrência e uma política de reserva. Para executar uma tarefa apenas uma vez em um ambiente distribuído, a entrega real costuma ser pelo menos uma vez; o consumidor deve tolerar duplicados.
Design da chave e do registro persistente
O cliente deveria gerar uma chave opaca e suficientemente imprevisível quando nasce a intenção de negócio, mantê-la enquanto puder tentar novamente e enviá-la, por exemplo, em Idempotency-Key. Se o servidor a gerar a cada recebimento, não poderá vincular uma repetição posterior. Em fluxos internos, a chave pode ser derivada de um identificador estável do evento de negócio.
Seu escopo deve incluir o ator ou tenant e a operação. A mesma string não deve colidir entre duas contas nem entre «criar pedido» e «emitir reembolso». Defina uma retenção alinhada ao período real de novas tentativas e aos riscos do domínio. Excluir o registro cedo demais reabre a possibilidade de duplicação; mantê-lo indefinidamente aumenta o custo e exige uma política de privacidade e exclusão.
Um modelo mínimo de persistência inclui:
- escopo de segurança ou tenant, nome da operação e chave idempotente;
- hash criptográfico de uma carga útil normalizada;
- estado:
processing,completed,failedoupendingquando a confirmação externa é incerta; - código e corpo da resposta que serão devolvidos de forma repetível;
- identificadores do recurso criado, correlação interna e referência do provedor externo;
- datas de criação, atualização e expiração.
O hash evita um erro importante: reutilizar a mesma chave com dados diferentes. Nessa situação, responda com um conflito e não processe a nova carga. Para que a comparação seja confiável, normalize campos cuja ordem não tenha significado e exclua metadados variáveis que não façam parte da intenção.
Fluxo PHP: reservar antes de produzir o efeito
A proteção deve ser respaldada por uma restrição única no banco de dados sobre o escopo, a operação e a chave. Consultar primeiro e inserir depois não é suficiente: duas solicitações simultâneas podem observar a ausência do registro e continuar ao mesmo tempo.
O fluxo recomendado é reservar de forma atômica. Se a inserção for bem-sucedida, esse processo será o proprietário inicial da execução. Se houver conflito de unicidade, lê-se o registro existente, verifica-se o hash e age-se conforme o seu estado. Um resultado concluído devolve exatamente a resposta persistida; uma operação em andamento pode devolver um estado pendente ou aguardar apenas um intervalo limitado antes de consultar novamente.
begin transaction
insert idempotency_records(scope, operation, key, payload_hash, status)
values (?, 'create_order', ?, ?, 'processing')
-- a restrição única decide o proprietário
commit
if reservation_was_created:
result = execute_business_operation()
persist_completed_response(result)
else:
record = load_existing_record()
assert_same_payload_hash(record)
return replay_or_pending(record)Não mantenha uma transação nem um bloqueio de linha abertos durante uma chamada lenta a um provedor. Isso reduz a capacidade e pode gerar bloqueios prolongados. Em vez disso, reserve e confirme o estado local em transações curtas. Se o efeito externo e o registro local precisarem ser coordenados, armazene também uma ordem de envio em uma tabela transacional e processe-a separadamente. Esse padrão não elimina as novas tentativas, mas permite recuperar o trabalho pendente sem perder a intenção registrada.
Concorrência, timeouts e estados incertos
Duas solicitações com a mesma chave podem chegar com milissegundos de diferença. A restrição única estabelece qual delas reserva a operação. A segunda não deve iniciar outro efeito externo. Ela pode responder 202 enquanto o estado for processing ou pending, incluindo um identificador para consultar o resultado; se o contrato exigir uma resposta síncrona, pode fazer uma espera limitada e reler o registro.
Uma falha antes de iniciar qualquer efeito permite marcar failed com um erro reproduzível. No entanto, um timeout ao chamar um sistema externo cria incerteza: não é correto marcar automaticamente como falha nem reenviar uma ordem sem mais. Guarde a referência da solicitação enviada, se ela existir, consulte o provedor por meio dessa referência e reconcilie o resultado. Enquanto não houver confirmação, mantenha pending e informe que o resultado ainda não é definitivo.
A chamada externa também precisa de uma referência estável. Se o provedor aceitar sua própria chave idempotente, propague uma chave associada à mesma intenção. Caso não aceite, use identificadores de comércio, leitura posterior, reconciliação periódica e procedimentos operacionais para os casos ambíguos. Nenhuma transação local pode tornar atômicas uma gravação no banco de dados e uma API remota independente.
O que uma chave de idempotência não resolve
A idempotência evita repetir uma intenção reconhecida; ela não decide como desfazer um efeito irreversível. Um envio físico, uma transferência já liquidada ou uma notificação vista por um usuário podem exigir compensação, cancelamento ou atendimento manual. Projete essas ações como processos de negócio explícitos, com permissões, estados e auditoria.
Também não confunda uma correção com uma nova tentativa. Se o usuário mudar endereço, valor ou produtos após um erro, haverá uma nova intenção e ele deverá usar uma nova chave. Reutilizar a anterior com outra carga deve gerar conflito, não atualizar silenciosamente a operação original.
Testes, observabilidade e lista de verificação

Teste mais do que o caminho feliz. Interrompa a resposta após persistir o resultado, repita a mesma chave em paralelo, reinicie um worker após reservar o registro e simule um timeout depois de enviar uma solicitação externa. Verifique que existe apenas um recurso de negócio, que a resposta repetida mantém o mesmo resultado e que uma carga diferente com a mesma chave não é aceita.
Registre, sem expor dados sensíveis, a chave ou um identificador seguro derivado, o escopo, o estado, a correlação e a referência externa. As métricas de conflitos de chave, operações pendentes por tempo excessivo e reconciliações não resolvidas ajudam suporte e operações a distinguir uma nova tentativa normal de uma ocorrência.
- A chave representa uma intenção de negócio e tem um escopo definido?
- Existe uma restrição única que impede duas reservas concorrentes?
- Um hash da carga é comparado e mudanças de intenção são rejeitadas?
- Uma resposta ou resultado que possa ser repetido de forma coerente é persistido?
- Os estados incertos permitem consultar e reconciliar antes de tentar novamente?
- Cada efeito externo tem referência, recuperação e alternativa operacional?
- Foram testados duplicados, falhas, novas tentativas da fila e concorrência real?
Aplicada dessa forma, a idempotência não promete que uma rede seja confiável. Ela faz com que falhas inevitáveis tenham um resultado controlável, rastreável e coerente para o negócio.



