Every team I have worked with eventually hits the same incident. A client times out, retries, and a customer ends up with two charges, two emails, or two of whatever the endpoint creates. The post-mortem proposes “we should add idempotency keys.” The work gets scoped, deprioritized, and then the next incident happens.

Idempotency is not a retrofit. It is part of the contract. Treating it as a feature you ship later means every consumer downstream of your API is silently coupled to the assumption that retries are unsafe, and that assumption leaks into queues, jobs, and human runbooks.

What idempotency actually means in an API

The HTTP spec defines idempotency in terms of side effects: making the same request N times has the same observable effect as making it once. GET, PUT, and DELETE are idempotent by definition. POST is not, and that is where most of the pain lives.

Making POST safely retriable requires the server to recognize “I have seen this request before” and return the original result instead of doing the work a second time. That recognition needs:

  1. A key that the client controls and can repeat across retries.
  2. A server-side record of (key -> result) with a defined lifetime.
  3. A rule for what happens when the same key arrives with a different body.

Skip any of these and the behavior is a footgun.

Designing the key

The convention that has held up best is an Idempotency-Key header carrying an opaque, client-generated value (typically a UUIDv4 or v7). Two notes from experience:

Storing the result

The server-side record needs three fields: the key, a fingerprint of the request body, and the response (status code plus body) of the first successful processing. On a repeat request:

Lifetime is the part most designs get wrong. Twenty-four hours is too short for some workflows (batch retries the next day) and too long for others (the key table grows unbounded). Pick a value tied to your retry policy, document it in the API reference, and make the TTL visible in the response (Idempotency-Replayed: true, or similar).

What about the underlying write?

The idempotency layer protects the API surface. It does not, by itself, protect the database. Two patterns work well together:

Common mistakes worth calling out

Where this leaves you

If your API has any POST that creates a resource, sends money, or triggers an external effect, treat idempotency as part of v1 of that endpoint. The cost is a header, a small table, and a few branches in the handler. The cost of adding it after the fact is every consumer’s retry logic and a handful of customer-visible incidents.

It is one of the highest-leverage things you can put in an API contract. Worth doing once, properly, up front.