How to Implement Idempotent API Endpoints

An idempotent endpoint produces the same result no matter how many times an identical request is made — critical for handling network retries, duplicate webhook deliveries, and client-side double-submissions safely.

Why Idempotency Matters

Networks are unreliable — a client might not receive a response even though the server successfully processed the request, causing the client to retry. Without idempotency, that retry could create a duplicate order, charge a customer twice, or send a duplicate email.

HTTP Methods and Idempotency by Default

MethodIdempotent by Convention?Notes
GETYesReading data shouldn't have side effects
PUTYesSetting a resource to a specific state, repeatable safely
DELETEYesDeleting an already-deleted resource has the same end state
POSTNo (by default)Typically creates a new resource each time — needs explicit idempotency handling

POST is the method most commonly requiring explicit idempotency work, since it inherently represents "create a new thing."

Implementing Idempotency Keys

The standard pattern: the client generates a unique key per logical operation and includes it in the request; the server tracks which keys it has already processed.

Client Side

const idempotencyKey = crypto.randomUUID();

fetch('/api/orders', {
  method: 'POST',
  headers: {
    'Idempotency-Key': idempotencyKey,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ productId: 123, quantity: 2 }),
});

Critically, if the client retries the same logical request (e.g. due to a timeout), it reuses the same idempotency key — not a new one.

Server Side

app.post('/api/orders', async (req, res) => {
  const idempotencyKey = req.headers['idempotency-key'];

  if (!idempotencyKey) {
    return res.status(400).json({ error: 'Idempotency-Key header required' });
  }

  const existing = await db.query(
    'SELECT response FROM idempotency_keys WHERE key = ?',
    [idempotencyKey]
  );

  if (existing.length > 0) {
    return res.status(200).json(JSON.parse(existing[0].response));
  }

  const order = await createOrder(req.body);

  await db.query(
    'INSERT INTO idempotency_keys (key, response, created_at) VALUES (?, ?, NOW())',
    [idempotencyKey, JSON.stringify(order)]
  );

  res.status(201).json(order);
});

Handling Concurrent Requests with the Same Key

If two identical requests arrive nearly simultaneously (before the first has finished processing), a race condition could still create a duplicate. Use a database unique constraint on the idempotency key combined with proper transaction handling to prevent this:

CREATE TABLE idempotency_keys (
  `key` VARCHAR(255) PRIMARY KEY,
  response TEXT,
  created_at TIMESTAMP
);

The unique constraint on key causes the second concurrent insert to fail, which your application code can catch and handle by returning the already-in-progress or completed result instead.

Expiring Old Idempotency Keys

DELETE FROM idempotency_keys WHERE created_at < NOW() - INTERVAL 24 HOUR;

Schedule via cron — retaining keys indefinitely isn't necessary, since clients typically only retry within a limited window after the original request.

Idempotency for Webhooks

The same pattern applies to processing incoming webhooks — see How to Design and Secure Webhook Endpoints, using the event's own unique ID from the webhook provider as the idempotency key.

Making Database Operations Naturally Idempotent

Where possible, design the operation itself to be naturally idempotent, avoiding the need for explicit key tracking:

-- Naturally idempotent: setting a specific state
UPDATE orders SET status = 'shipped' WHERE id = 123;

-- Not naturally idempotent: incrementing
UPDATE inventory SET quantity = quantity - 1 WHERE id = 123;

Common Errors

Duplicate resources still created despite idempotency key logic — check for a race condition; ensure a database-level unique constraint backs the idempotency check, not just an application-level lookup that can race.

Best Practices

  • Require idempotency keys for any POST endpoint with real-world side effects (payments, order creation)
  • Back idempotency checks with a database unique constraint, not just application logic
  • Expire old idempotency keys on a reasonable schedule

Continue Reading

Browse more articles in Object Storage, Messaging & APIs.

  • idempotency, idempotent api, api reliability, duplicate request handling
  • 0 Usuários acharam útil
Esta resposta lhe foi útil?

Artigos Relacionados

How to Set Up Self-Hosted S3-Compatible Object Storage with MinIO

MinIO is a high-performance, self-hosted object storage server compatible with the S3 API —...

How to Use Object Storage for Application File Uploads

Storing user-uploaded files directly on your application server's disk creates scaling and...

How to Install and Configure RabbitMQ on a VPS

RabbitMQ is a widely-used, robust message broker — enabling applications to communicate...

How to Install and Configure Redis as a Message Queue

Redis, primarily known as a cache, also works well as a lightweight message queue for simpler use...

How to Build and Secure a REST API on a VPS

This guide covers the essential security and architecture practices for deploying a REST API on...