Live · Fri, Sep 25, 2026 · 17:01 UTC Block 843,917 Fees 14 sat/vB Fear & Greed 72 · Greed
Newsletter Pro Terminal Sign in
ITop Field News.
Subscribe →
Live · 17:01 UTC Block 843,917 F&G 72
Software development Software development desk

Idempotency in API design: what it means and why it breaks

Idempotency is one of those API design concepts teams assume they understand until a retry storm hits production and orders get duplicated. Here's a clear breakdown of what it means and where implementations quietly fall apart.

A modern server room featuring network equipment with blue illumination. Ideal for technology themes.

Photo by panumas nikhomkhai on Pexels

Idempotency is the property that lets you call an API operation multiple times and get the same result as calling it once. For payment APIs, order systems, and anything that writes to a database, that guarantee is what separates a resilient service from one that silently duplicates records or charges customers twice. Most development teams know the definition. Far fewer have implemented it correctly.

What idempotency actually means in practice

The formal definition comes from mathematics, but the practical version is simpler: if a client retries a request, the server produces the same outcome it would have produced the first time. No extra rows. No second charge. No duplicate notification sent to a user.

HTTP methods each carry a different idempotency guarantee by convention. GET, PUT, DELETE, and HEAD are all defined as idempotent. POST is not. That distinction matters because it tells clients whether a retry is safe after a network failure. A timed-out GET is harmless to retry; a timed-out POST might not be, unless the server explicitly handles it.

The word "by convention" is doing real work in that last paragraph. An HTTP GET that triggers a database write is not idempotent, no matter what the spec says about the method. Idempotency is a server-side property, enforced by code, not assumed from the HTTP verb.

The idempotency key pattern

The standard approach to making non-idempotent operations safe is the idempotency key: a client-generated unique identifier sent with the request, typically in a header like Idempotency-Key. The server stores the result of the first successful request against that key. On any subsequent request with the same key, the server returns the stored result rather than executing the operation again.

Stripe made this pattern widely known through its payments API, and most payment processors now implement something similar. The mechanics look straightforward. The failure modes are not.

Three things trip teams up when implementing idempotency keys:

  • Key expiry: if you expire keys too quickly, a client retrying after a network partition may get a fresh execution rather than the original result.
  • Partial failure: if the operation succeeds but writing the idempotency key to storage fails, you have an untracked successful operation that will execute again on the next retry.
  • Scope confusion: if two different clients generate the same key by accident, you have either a collision or a security issue depending on how the server partitions keys.

The partial failure case is the most dangerous. It requires storing the idempotency key and the operation result atomically. That means a transaction, or at minimum a careful sequence that writes the key before executing the operation. Writing the key after means there's always a window where the operation completes but the key isn't stored.

Where team implementations quietly go wrong

Most breakdowns don't come from ignoring idempotency. They come from applying it inconsistently. A payment endpoint gets an idempotency key implementation because the team knows a duplicate charge is a serious problem. The inventory deduction endpoint does not, because someone assumed the load balancer would deduplicate retries. It won't.

Distributed systems fail in ways that guarantee retries. Load balancers time out and retry. API gateways retry on 5xx. Mobile clients retry on connection drop. If any of those retries reaches a non-idempotent write endpoint, you get duplicate state. The fix isn't asking clients to retry less. It's making the server safe to retry against.

Database-level deduplication is a common shortcut. A unique constraint on an order reference or a transaction ID will reject a true duplicate at the database layer, surfacing as a conflict the application can catch and handle gracefully. This works for simple cases. It doesn't handle the scenario where the same logical operation has a different database representation each time, which is common in systems that generate IDs server-side.

Caching layers introduce another subtle failure. Some teams cache API responses and return the cached response on a retry, which looks like idempotency but isn't. A cache can serve a stale result from a previous different request. A true idempotency store is keyed specifically to the client's idempotency key, not to the request parameters.

Idempotency and API versioning

One issue that gets less attention is how idempotency keys interact with API versioning. If a client sends the same idempotency key to two different versions of an endpoint, the stored result from v1 may be served in response to a v2 request that expects a different response shape. This causes client-side parsing failures that look like server bugs.

The solution is to include the API version in the idempotency key namespace, either by prefixing stored keys with the version or by treating keys as version-scoped. If you're thinking through how your API should handle versioning more broadly, API versioning strategies covers the trade-offs between URL versioning, header versioning, and content negotiation in depth.

Testing idempotency correctly

Most API test suites check that an endpoint returns the right response to a single correct request. Almost none check idempotency explicitly. Testing idempotency requires sending the same request twice with the same key and asserting that the side effects happened exactly once, not that the response body is identical.

For a payment endpoint, that means checking the database for a single charge record. For an email trigger, it means checking that exactly one email was queued. Response body equality is a necessary but not sufficient condition. A server can return the same 200 response twice while executing the operation twice.

Integration tests that exercise retry paths are rare because they require simulating network failures. One practical approach: build a middleware layer in test environments that wraps every mutating handler and injects a forced retry after the first execution. Any handler that isn't idempotent will surface a duplicate side effect immediately.

Rate limiting and idempotency

There's a design tension between rate limiting and idempotency that's worth naming. Rate limiters count requests. Idempotent retries are semantically the same request. If your rate limiter counts a retry as a new request, a client caught in a retry loop during a network outage will hit your rate limit and receive 429 responses before the underlying operation resolves.

The cleaner approach is to exempt idempotency-keyed retries from rate limiting, or to count the key rather than the request. API rate limiting design covers the implementation trade-offs in detail, including sliding window versus token bucket approaches that change how retry exemptions interact with the overall limit.

Idempotency in event-driven systems

REST APIs are the obvious context for idempotency discussions, but the same problem appears in event-driven architectures. Message queues deliver at-least-once by default. Consumer services need to handle duplicate messages without duplicating effects, which is exactly the same idempotency problem under a different name.

The standard pattern is deduplication at the consumer: track processed message IDs in a store, check before processing, skip if already seen. The failure modes are the same as idempotency keys: you need to write the processed ID and commit the side effects atomically. An outbox pattern handles this cleanly by writing both the result and the deduplication record within the same database transaction, then publishing to the queue after commit.

Getting idempotency right across both synchronous APIs and async consumers requires deliberate design from the start. Retrofitting it after a production incident is significantly harder, particularly in systems where the idempotency boundary crosses service boundaries and each service owns its own storage.

→ The Confirmations · Daily newsletter

One email at 06:00 UTC. Six minutes. The only digest written for desks, not for retail.