Tags: web-dev concept

Idempotency

Date: 2026-08-17


An operation safe to repeat: doing it twice leaves the same state as doing it once. Every system that retries needs it, and networks guarantee that everything eventually gets retried — including by users pressing the button again.


An idempotent operation produces the same resulting state however many times it is applied.

IDEMPOTENT              NOT IDEMPOTENT
set stock to 5          decrement stock by 1
mark order as paid      add £10 to balance
delete order 42         create an order
set email to X          append to a list

Note the pattern: absolute assignments are idempotent, relative changes aren’t. That’s the single most useful heuristic here.

Why it’s unavoidable

A client that doesn’t receive a response cannot tell these apart:

CLIENT                    SERVER
POST /orders  ──────────→ order created ✓
              ✗ timeout ← response lost

client sees: failure
server sees: success

The client must retry — it has no other option — and without idempotency that retry creates a second order. This isn’t an edge case; it’s the normal behaviour of mobile networks, and it’s why duplicate orders exist at every retailer.

Idempotency keys

The standard mechanism, and the one payment providers require:

1  client generates a unique key per
   ATTEMPT (a UUID), not per retry

2  POST /orders
   Idempotency-Key: 3f2a...

3  server checks: seen this key?
     no  → do the work, store
           key → result
     yes → return the STORED result,
           do nothing

4  retries carry the same key
   → one order, correct response

The key must be generated once, when the user acts — not regenerated per attempt, which would defeat the whole thing.

Two details that get missed:

  • Store the response, not just the key. A retry should return the original result, not a bare “already done”
  • Expire keys on a stated window — 24 hours is common. Keeping them forever is a growing table with no upside

HTTP’s contract

The method semantics say which are idempotent, and this is what makes proxies, browsers and load balancers safe to retry:

GET      safe + idempotent
HEAD     safe + idempotent
PUT      idempotent      (full replacement)
DELETE   idempotent      (second one: 404
                          or 204, same state)
POST     NOT idempotent  ← needs a key
PATCH    depends on the payload

“Safe” means no state change at all. A GET that mutates something breaks prefetching, caching and every crawler assumption — HTTP Semantics.

PATCH is the subtle one. {"status": "paid"} is idempotent; {"stockDelta": -1} isn’t. Same method, different answer, decided by the body.

Where it’s required

  • Payments. Every provider mandates an idempotency key. Charging twice is the worst bug in commerce
  • Webhooks. Delivered at least once and unordered. Every handler must tolerate duplicates — deduplicate on the event ID — Reference - AEM
  • Message queues. Same guarantee, same requirement
  • Analytics events. A retried event is an inflated conversion count, which is why event IDs and deduplication windows exist — Double Counting
  • Database migrations. A migration that fails halfway must be safe to run again — Database Migrations
  • Deploys and infrastructure. The whole appeal of declarative configuration is that applying it twice is a no-op

Designing for it

  • Prefer absolute over relative. quantity = 3 survives retries; quantity += 1 doesn’t
  • Let the client supply the ID. If the client generates the order ID, a retry with the same ID is naturally deduplicated by the primary key
  • Unique constraints in the database are the only reliable dedup. An application-level check has a gap — Race Conditions
  • Make delete tolerant. Deleting something already gone should succeed, not error
  • Design the retry policy alongside the endpoint, not afterwards. Retries with exponential backoff plus jitter, and a cap — Error Handling Strategies

At-least-once delivery plus idempotent handling equals exactly-once processing. Genuine exactly-once delivery is effectively unavailable in a distributed system, so this pairing is how the industry gets the practical equivalent — and it’s why “make the handler idempotent” is the standard answer to almost every duplicate-processing question.