Idempotency: The Property That Saves Your API From Itself
Networks fail. Clients retry. Users double-click submit buttons out of impatience. None of this is unusual — it's the normal, messy reality of distributed systems. What separates a fragile API from a resilient one isn't whether these things happen, but what occurs when they do. That's where idempotency comes in: a property that's easy to overlook in a happy-path demo, and absolutely critical the moment real users and real networks show up.
What idempotency actually means
An operation is idempotent when performing it multiple times produces the same result as performing it once.
GET /users/42 is naturally idempotent — asking for the same user repeatedly doesn't change anything. DELETE /users/42 is also idempotent in practice: deleting an already-deleted resource should leave the system in the same state (resource gone), even if the HTTP status code on the second call differs.The classic trouble case is
POST. By HTTP convention, POST is not idempotent — and that's exactly the problem. A POST /payments endpoint that charges a customer will, by default, create a new charge every time it's called, even if the caller's intent was "make this payment happen," not "make this payment happen as many times as this request gets sent."Why this matters more than it seems
Consider a mobile app making a payment request over a flaky connection. The request reaches the server, the payment is processed, but the response never makes it back to the client because the connection drops. From the client's perspective, the request failed — so, reasonably, it retries. Without idempotency protection, the customer now has two charges instead of one.
This isn't a rare edge case. It's the default behavior of distributed systems: timeouts, retries, duplicate messages, and out-of-order delivery are the norm, not the exception. An API that assumes every request arrives exactly once is an API built for a network that doesn't exist.
How idempotency is typically implemented
The most common pattern is the idempotency key: the client generates a unique identifier (often a UUID) for a given logical operation and sends it along with the request, usually as a header (
Idempotency-Key: <uuid>). The server then:- Checks whether it has already processed a request with that key.
- If yes, returns the stored result from the original request instead of re-executing the operation.
- If no, processes the request normally and stores the result against that key, typically with an expiration window.
This shifts the responsibility to the server: no matter how many times the same logical request arrives, the side effect happens exactly once. The client can safely retry on any failure or timeout without worrying about duplicating the operation.
Idempotency is a design decision, not an afterthought
It's tempting to treat idempotency as something to "add later if it becomes a problem." In practice, retrofitting it is much harder than designing for it from the start, because:
- The database schema often needs a way to store and look up idempotency keys efficiently.
- Business logic needs to distinguish "this is a genuine duplicate" from "this is a new, unrelated operation with coincidentally similar data."
- Edge cases multiply: what happens if two requests with the same key arrive simultaneously? What if the original request is still being processed when the retry comes in?
Designing for idempotency from day one — at least for any endpoint with a meaningful side effect (payments, order creation, sending notifications) — avoids a painful migration later and, more importantly, avoids the incident report that comes from a duplicated charge in production.
Where it matters most
Not every endpoint needs this level of care. The ones that do share a pattern: they create or mutate state with real-world consequences if duplicated.
- Payment and billing operations.
- Order creation in e-commerce systems.
- Sending emails, SMS, or push notifications.
- Any operation triggered by a message queue, where at-least-once delivery is the norm rather than the exception.
For purely read operations, or for mutations where duplication is harmless (toggling a UI preference, for instance), the extra complexity of idempotency keys usually isn't worth it.
Bottom line
Idempotency isn't a nice-to-have for APIs that expect to run in the real world — it's a baseline requirement for anything with a side effect that matters. Networks will fail, clients will retry, and messages will arrive more than once. The question isn't whether that will happen, but whether the system was built to handle it gracefully or to fall over the first time it does.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article