Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Idempotência em APIs Node.js

Atualizado em: 21 de agosto de 2026

Rack de servidores processando fluxos de dados no Node.js

A idempotência em APIs Node.js evita que uma mesma operação produza efeitos duplicados quando o cliente repete uma requisição. Repetições acontecem por timeout, reconexão, retry automático, falha de proxy, clique duplo ou ausência de confirmação da resposta. Sem proteção, um pedido pode ser criado duas vezes, um pagamento pode ser cobrado novamente ou um e-mail pode ser enviado em duplicidade.

Uma operação idempotente retorna o mesmo resultado lógico quando é executada repetidamente com a mesma intenção. Métodos como GET, PUT e DELETE possuem semânticas idempotentes no HTTP, mas a implementação precisa respeitar essa propriedade. POST normalmente não é idempotente, porém pode receber uma chave de idempotência para garantir processamento único.

Neste guia, você aprenderá a criar chaves, armazenar resultados, validar payloads, lidar com concorrência, transações, expiração, erros, retries, webhooks e testes.

O que é idempotência?

Uma operação é idempotente quando repetir a mesma solicitação não altera o estado além do efeito da primeira execução. O RFC 9110 sobre semântica HTTP descreve métodos idempotentes. A documentação da Stripe sobre requisições idempotentes mostra um exemplo amplamente utilizado em APIs de pagamento.

Para retries, consulte Retry com Backoff no Node.js. Para eventos recebidos mais de uma vez, veja Webhooks seguros com Node.js.

Por que requisições são repetidas?

  • o cliente não recebeu a resposta;
  • o proxy encerrou a conexão;
  • um timeout ocorreu após o commit;
  • o usuário clicou duas vezes;
  • um worker reiniciou durante processamento;
  • uma fila entregou a mensagem novamente;
  • o SDK aplicou retry automático;
  • o aplicativo móvel alternou de rede.

O cliente não consegue distinguir facilmente entre “o servidor não processou” e “o servidor processou, mas a resposta se perdeu”.

Métodos HTTP

GET deve apenas ler. PUT representa substituição ou definição de um recurso conhecido e deve produzir o mesmo estado quando repetido. DELETE deve manter o recurso ausente após repetições. POST geralmente cria um novo recurso a cada chamada.

POST com chave de idempotência

POST /api/orders
Idempotency-Key: 5f7e3bd8-9bb8-4fe2-a690-caaeb771d175

O cliente gera uma chave única por operação lógica. Se precisar repetir a mesma operação, reutiliza a mesma chave.

Formato da chave

UUID aleatório é uma opção comum:

const key = crypto.randomUUID();

Defina tamanho máximo e caracteres permitidos. Não use informações pessoais, token de acesso ou dados previsíveis como única proteção.

Escopo da chave

A mesma chave pode ser válida por usuário, conta, endpoint ou operação. Um índice composto evita colisões entre clientes:

UNIQUE (account_id, endpoint, idempotency_key)

Documente o escopo para que SDKs saibam quando podem reutilizar o valor.

Tabela de idempotência

CREATE TABLE idempotency_keys (
  account_id BIGINT NOT NULL,
  endpoint TEXT NOT NULL,
  idempotency_key TEXT NOT NULL,
  request_hash TEXT NOT NULL,
  status TEXT NOT NULL,
  response_status INTEGER,
  response_body JSONB,
  resource_id BIGINT,
  created_at TIMESTAMPTZ NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL,
  PRIMARY KEY (account_id, endpoint, idempotency_key)
);

A tabela registra intenção, andamento e resultado.

Hash do payload

A mesma chave não deve aceitar payload diferente:

const canonicalPayload = JSON.stringify({
  productId,
  quantity,
  addressId
});

const requestHash = createHash('sha256')
  .update(canonicalPayload)
  .digest('hex');

Canonicalização precisa ser determinística. Ordem de propriedades e valores padrão devem ser tratados antes do hash.

Conflito de payload

Se a chave existe com outro hash, retorne erro:

{
  "code": "IDEMPOTENCY_KEY_REUSED",
  "message": "A chave já foi usada com outra requisição"
}

Um status 409 Conflict costuma ser apropriado.

Fluxo básico

  1. validar a chave;
  2. normalizar e calcular hash;
  3. tentar registrar a chave;
  4. se já existe, comparar hash;
  5. se concluída, retornar resultado salvo;
  6. se em andamento, aguardar ou retornar conflito;
  7. executar a operação;
  8. salvar resposta;
  9. realizar commit.

Concorrência

Duas requisições com a mesma chave podem chegar ao mesmo tempo. Um simples SELECT seguido de INSERT possui race condition.

INSERT INTO idempotency_keys (...)
VALUES (...)
ON CONFLICT DO NOTHING;

Depois, verifique se esta transação criou a linha ou se outra requisição venceu.

Transação única

Quando possível, crie o recurso e registre o resultado na mesma transação:

BEGIN;

INSERT INTO idempotency_keys (...);
INSERT INTO orders (...) RETURNING id;
UPDATE idempotency_keys
SET status = 'completed', resource_id = $1
WHERE ...;

COMMIT;

Consulte Pool PostgreSQL no Node.js para uso correto de clientes e transações.

Estado em processamento

Uma chave pode ficar com status processing. Se outra chamada chegar, a API pode:

  • retornar 409 ou 425;
  • aguardar por um período curto;
  • consultar o recurso relacionado;
  • retornar 202 com endpoint de status.

Evite manter a conexão aberta indefinidamente.

Falha antes do efeito

Se a validação falha antes de qualquer alteração, você pode não registrar a chave ou salvar a resposta de erro. Defina uma política consistente.

Falha após o efeito

Esse é o caso mais difícil. O banco pode ter confirmado o pedido, mas o processo caiu antes de salvar a resposta. A solução é manter efeito e registro na mesma transação ou conseguir reconstruir o resultado pelo recurso criado.

Outbox pattern

Se a operação também publica evento, grave o evento em uma outbox na mesma transação. Um worker publica depois, garantindo que o commit do pedido não dependa diretamente do broker.

Resposta armazenada

{
  "responseStatus": 201,
  "responseBody": {
    "id": 842,
    "status": "created"
  }
}

Não armazene headers dinâmicos ou segredos sem necessidade.

Resposta atual versus resposta original

Uma repetição pode devolver exatamente a resposta original ou reconstruir o estado atual do recurso. Para contratos previsíveis, retornar a resposta original costuma ser mais simples.

Expiração

Chaves não precisam existir para sempre. Defina retenção conforme o tempo máximo de retry:

expires_at = now() + interval '24 hours'

Pagamentos e operações críticas podem exigir prazo maior. Depois da expiração, a mesma chave pode ser tratada como nova, portanto documente o comportamento.

Limpeza

DELETE FROM idempotency_keys
WHERE expires_at < now()
  AND status != 'processing';

Execute em lotes para evitar locks extensos.

Redis

Redis pode registrar chaves com SET NX e TTL:

SET idempotency:key processing NX EX 86400

Mas se o efeito principal está no PostgreSQL, coordenar Redis e banco sem transação distribuída cria janela de inconsistência. Para operações críticas, prefira o mesmo banco do efeito.

Lock distribuído

Locks podem reduzir concorrência, mas não substituem constraint única e transação. Um lock expirado cedo permite duas execuções; um lock longo pode bloquear recuperação.

Idempotência por recurso natural

Às vezes existe uma chave de negócio:

UNIQUE (merchant_id, external_order_id)

Esse identificador impede pedidos duplicados mesmo sem tabela separada.

PUT idempotente

PUT /api/users/42/preferences

Enviar o mesmo documento várias vezes deve produzir o mesmo estado. Evite incrementar contadores ou adicionar itens como efeito escondido de PUT.

DELETE idempotente

A primeira chamada pode retornar 204. Repetições podem retornar 204 novamente ou 404, dependendo do contrato. O estado final continua sendo “recurso ausente”.

Webhooks

Provedores geralmente entregam eventos ao menos uma vez. Armazene o ID do evento com constraint única:

INSERT INTO processed_events (provider, event_id)
VALUES ($1, $2)
ON CONFLICT DO NOTHING;

O processamento e o registro devem ser atômicos quando possível.

Filas

RabbitMQ, Kafka e outros sistemas podem redeliver mensagens. Consumidores precisam ser idempotentes mesmo com acknowledgements.

Consulte RabbitMQ com Node.js e Kafka com Node.js.

Retries do cliente

O cliente deve repetir apenas erros transitórios, usando a mesma chave:

for (let attempt = 0; attempt < 3; attempt += 1) {
  try {
    return await createOrder({
      idempotencyKey,
      body
    });
  } catch (error) {
    if (!isRetryable(error)) throw error;
    await delay(backoff(attempt));
  }
}

Timeout

Um timeout não significa falha do servidor. O cliente deve consultar o status ou repetir com a mesma chave, nunca gerar outra imediatamente.

Validação e autorização

Valide autenticação antes de procurar a chave. Um usuário não deve recuperar resposta idempotente de outra conta.

Rate limiting

Idempotência não elimina abuso. Um atacante pode gerar milhões de chaves diferentes. Aplique rate limiting, tamanho máximo e quota de armazenamento.

Veja Rate Limiting no Node.js.

Documentação OpenAPI

Documente o header:

parameters:
  - in: header
    name: Idempotency-Key
    required: true
    schema:
      type: string
      maxLength: 128

Explique retenção, conflito e comportamento de respostas repetidas. Consulte OpenAPI com Node.js.

Métricas

Monitore:

  • chaves novas;
  • repetições atendidas;
  • conflitos de payload;
  • operações em processamento;
  • tempo de retenção;
  • falhas após commit;
  • limpeza por lote.

Não use a chave como label de métrica.

Logs

Registre um hash curto ou identificador sanitizado, não o payload completo. Inclua resultado: nova, repetida, conflito ou em andamento.

Testes

Cubra:

  • primeira execução;
  • repetição após sucesso;
  • mesma chave com payload diferente;
  • duas chamadas concorrentes;
  • falha antes do commit;
  • falha depois do efeito;
  • expiração;
  • usuários diferentes;
  • retry após timeout;
  • evento duplicado.

Teste concorrente

test('cria apenas um pedido', async () => {
  const key = crypto.randomUUID();

  const results = await Promise.allSettled([
    createOrder(key, payload),
    createOrder(key, payload)
  ]);

  const orders = await countOrdersByExternalId(
    payload.externalId
  );

  assert.equal(orders, 1);
  assert.equal(results.length, 2);
});

Erros comuns

  • SELECT antes de INSERT: ocorre race condition.
  • Não comparar payload: a chave aceita outra operação.
  • Usar Redis separado: efeito e chave podem divergir.
  • Gerar nova chave no retry: a operação duplica.
  • Sem expiração: a tabela cresce indefinidamente.
  • Salvar somente sucesso: falhas ambíguas são reexecutadas.
  • Escopo global: contas diferentes colidem.

Boas práticas

  • Use chave única por intenção.
  • Defina escopo.
  • Calcule hash do payload.
  • Use constraint única.
  • Execute efeito e registro em transação.
  • Salve o resultado.
  • Defina expiração.
  • Limite tamanho e quantidade.
  • Documente retries.
  • Teste concorrência e falhas.

Conclusão

A idempotência em APIs Node.js transforma retries inevitáveis em operações seguras. Uma chave associada ao cliente, endpoint e payload permite reconhecer a intenção original e devolver o resultado sem repetir o efeito.

A implementação robusta exige constraint única, transação, hash do payload, retenção e testes concorrentes. Quando combinada com backoff, outbox e consumidores idempotentes, ela protege pedidos, pagamentos e integrações contra duplicidade mesmo em ambientes distribuídos sujeitos a falhas.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita