Network requests fail. Clients time out, load balancers drop connections, and mobile devices switch networks mid-request. When a client is unsure whether a request succeeded, it retries. Whether that retry is safe depends on one property of the operation: idempotency.
An operation is idempotent if performing it multiple times has the same effect on server state as performing it once. Understanding idempotency is essential for designing HTTP APIs that behave predictably under retries, for writing reliable client libraries, and for avoiding duplicate charges, duplicate records, and corrupted state.
This guide explains idempotency as defined by HTTP semantics, how each common method behaves, patterns for making non-idempotent operations safe, and practical limitations you should document for API consumers.
The HTTP definition
RFC 7231 defines safe and idempotent methods. A safe method is one that does not modify server state (in the sense intended by the specification). An idempotent method is one where "multiple identical requests with the same information" should be handled such that the server state does not change beyond the effect of a single successful request.
Important nuance: idempotency is about the intended effect on server state, not about identical HTTP responses. A second DELETE might return 404 where the first returned 204, yet both leave the resource absent—that is still idempotent in the REST sense.
Idempotency does not guarantee that responses are byte-identical. It guarantees that repeating the request does not accumulate extra side effects (extra rows, extra charges, extra emails).
Idempotency by HTTP method
GET and HEAD — safe and idempotent
GET retrieves a representation. Repeating a GET should not change server state. Clients, caches, and intermediaries may replay GET freely. This is why GET must never be used for actions that mutate data (a common anti-pattern in older web apps).
HEAD is like GET without a body. Same safety and idempotency properties.
PUT — idempotent
PUT replaces the resource at a target URI with the request payload. Sending the same PUT twice should leave the resource in the same final state. If the first request succeeds and the second is identical, the resource content should match what the first request established.
Caveats in practice:
- Timestamps and metadata: If the server updates
updated_aton everyPUT, the stored record differs, but the meaningful business fields may still be idempotent. Document whether metadata changes count as a new effect. - Partial representations: Some APIs treat
PUTas full replacement; others support patch-like behavior. Be explicit in your API contract. - Create-or-replace:
PUTto a new URI can create a resource; repeating the samePUTshould not create duplicates.
DELETE — idempotent
DELETE removes a resource. After a successful delete, further DELETE requests typically return 404 or 204, but the resource remains absent. That is idempotent: the end state is "gone," not "gone twice."
PATCH — not guaranteed idempotent
PATCH applies partial modifications. Whether it is idempotent depends entirely on the patch semantics you define.
Example of non-idempotent patch: { "op": "increment", "path": "/count", "value": 1 } — applying twice increments twice.
Example of idempotent patch: { "op": "replace", "path": "/status", "value": "archived" } — applying twice leaves status as archived.
If you expose PATCH, document which operations are idempotent or require clients to use conditional requests (If-Match with ETags) to avoid lost updates.
POST — not idempotent by default
POST is the general-purpose method for creating resources and triggering processing. Repeating the same POST usually creates duplicate resources or triggers duplicate side effects (two payments, two orders, two emails).
This is why payment and order APIs almost always add an idempotency key mechanism on top of POST.
Why retries make idempotency matter
Clients retry for many reasons:
- Timeouts: No response received; outcome unknown
- 5xx errors: Server error; request may or may not have been processed
- Connection resets: TCP failure after the server processed the request
For idempotent methods, automatic retry is often safe (with backoff and limits). For POST, blind retry is dangerous.
A typical failure sequence without idempotency protection:
- Client sends
POST /orderswith payment details - Server creates order and charges card
- Response is lost in transit
- Client retries
POST /orders - Server creates a second order and charges the card again
The user sees one action; the system performs two.
Idempotency keys for POST
The industry-standard pattern for safe POST retries is the idempotency key: a unique value the client generates for each logical operation and sends in a header (commonly Idempotency-Key).
How it works
- Client generates a unique key (UUID v4 is typical) for one logical operation
- Client sends
POSTwithIdempotency-Key: <key>and the request body - Server checks whether it has already processed this key (scoped to the authenticated client or account)
- If new: process the request, store the key with the response (or response hash), return result
- If duplicate: return the same response as the first successful processing (same status code and body), without re-executing side effects
Server storage requirements
The server must persist idempotency records long enough to cover realistic retry windows. Common choices:
- TTL of 24–72 hours for payment APIs
- Until terminal state for long-running workflows, with key reuse rules documented
Store at minimum: key, client identity, request fingerprint (hash of method, path, and body), response status, response body (or reference), and creation time.
Request fingerprinting
If the same idempotency key is reused with a different body, the server should reject the request with 422 or 409 rather than silently returning the first response. Otherwise clients cannot detect programming errors.
At-least-once delivery
Idempotency keys turn "at-least-once delivery" into "effectively once" from the client's perspective. The server may still receive duplicate requests; it must deduplicate on the key.
Conditional requests and concurrency
Idempotency addresses retries. Concurrency (two different clients updating the same resource) is a related but separate problem.
Use ETags with If-Match on PUT/PATCH/DELETE to ensure updates apply only if the resource version is unchanged. A failed 412 Precondition Failed tells the client to refetch and merge.
Combining idempotency keys (for POST retries) with ETags (for concurrent updates) covers most distributed client scenarios.
Idempotency in event-driven and message systems
HTTP idempotency has parallels in queues and streams:
- Message deduplication: Consumers track processed message IDs
- Exactly-once semantics: Often implemented as at-least-once delivery plus idempotent handlers
- Outbox pattern: Ensures publishing and database writes are consistent; consumers must still be idempotent
Design message handlers so that processing the same event twice does not corrupt state. Use natural idempotency (e.g., INSERT ... ON CONFLICT DO NOTHING) or explicit idempotency stores.
Designing idempotent operations intentionally
Sometimes you can make an operation idempotent by choice:
Use PUT with client-supplied IDs
Instead of POST /users that returns a server-generated ID, allow PUT /users/{client-generated-uuid} for creation. Retrying the same PUT creates or updates the same resource.
Trade-off: clients must generate collision-resistant IDs; you must validate uniqueness.
Upsert endpoints
Document PUT or PATCH endpoints that create if missing and update if present, with clear rules for which fields are set on create vs update.
Stateless command tokens
For workflows, accept a command ID in the body. Reject or replay commands with the same ID.
What to document in your API
API consumers cannot guess your idempotency behavior. Document explicitly:
- Which methods are idempotent for your resources
- Whether
PATCHoperations are idempotent and how Idempotency-Keyheader requirements: required vs optional, TTL, scope (per-user vs global)- Behavior on key reuse with different payloads
- Which status codes are safe to retry (generally
408,429,500,502,503,504with caution; not400unless documented) - Whether responses on retry are guaranteed identical or only semantically equivalent
Include examples of correct retry logic in your SDK or docs.
Common mistakes
Using GET for mutations. Breaks caching, prefetch, and crawler safety.
Assuming POST is idempotent because "it checks for duplicates." Duplicate detection by business fields (same email) is not the same as idempotency unless documented and guaranteed under concurrency.
Returning different responses for the same idempotency key. Clients rely on stable responses for retry logic.
Short TTL on idempotency records. Mobile clients on flaky networks may retry after long delays.
Ignoring partial failures. Order created but payment failed; retry must not create a second order. Model state machines and idempotency at each step.
Limitations
Idempotency keys require server-side storage and complicate horizontal scaling (shared store or sticky routing for key lookup). Very high-throughput systems must size this store appropriately.
True exactly-once semantics across heterogeneous systems (database + payment gateway + email) is difficult. Aim for idempotent effects at each boundary and accept that orchestration may require compensating transactions.
HTTP idempotency definitions describe ideal behavior; implementations vary. Always verify behavior with integration tests that simulate timeout and retry, not only happy-path calls.
Summary
Idempotent HTTP methods—GET, HEAD, PUT, DELETE—can be retried without multiplying side effects. POST is not idempotent by default; use idempotency keys, client-supplied resource IDs, or carefully designed upserts to make creation and command endpoints retry-safe. Document semantics clearly, test retry paths, and separate idempotency (same request twice) from concurrency control (two different requests at once). Reliable APIs are built for failure from the start.
