concept · foundation · 9 min read
Idempotency
In HTTP, idempotency means multiple identical requests have the same intended server effect as one request, allowing selected retries after uncertain network outcomes.
No prerequisites required.
Scope at a glance
Covers
- HTTP method semantics
- retry safety for intended server state
Does not cover
- exactly-once delivery guarantees
- database transaction isolation
Assumes
- The reader has general programming literacy; HTTP request, response, and retry terminology is introduced in the package.
Summary
Idempotency: running an operation once or many times produces the same final result. In HTTP, a request method is idempotent when multiple identical requests with that method have the same intended effect on the server as one such request. It does not require every repeat to return the same response body or status code, only the same intended effect.
It matters because network communication fails at uncertain moments. A client sends a request, and a timeout or a broken connection can hide whether the server applied it. If the request is idempotent, the client can resend it without asking the server for an effect beyond the first one.
Scope and assumptions
This package covers idempotency as a semantic property of HTTP methods and why that property makes selected retries safer. It also gives the minimal request, response, and retry context needed by a reader with general programming literacy.
It does not cover exactly-once delivery, transaction isolation, protection from concurrent changes, or deduplication for arbitrary application commands: those properties need separate contracts. Idempotent does not mean safe either: DELETE is idempotent and still asks to remove something.
Mental model
Think of the "Off" button on a light switch. If the light is on and you press it, it turns off. If you press it again, it stays off: the final state doesn't change even though you repeated the action. That operation is idempotent.
Compare that to a "Toggle" button: the first press turns it on, the second turns it off, the third turns it on again. Each execution changes the result. That operation is not idempotent.
The same reasoning applies to an HTTP request: you compare the intended server effect of one request against the intended effect of several identical requests. If both cases ask the server to reach the same result, the method is idempotent. The comparison is about what you're asking the server to do, not how many messages arrived.
Here's an example with a real API. A request sets an account's notification preference to email. The target account, requested representation, and semantics stay identical:
- The client sends the request.
- The server applies it and sets the preference to
email. - The response is lost before it reaches the client, which now doesn't know whether the update happened.
- The client resends the same request. The intended state is still "preference set to
email," same as after step 2.
The second request doesn't ask for a change beyond what the first one asked for: it's the "Off" button, not the "Toggle" button. This is different from a command like "append this event," where each identical request asks the server to add one more event — there, how many times you sent it does matter.
Responses can still differ. The first request might return success, while a later one might report that an HTTP precondition, a condition attached to the request, no longer holds, or that another client modified the resource. The server may also log each request separately. RFC 9110 allows these differences because idempotency is about the intended effect, not the exact response or internal side effects.
Practical use
Why this matters in practice, when a client decides whether to retry a request:
- ✅ Retrying a
PUTor aDELETEis safe: the repeat doesn't ask for an effect beyond the first request. - ✅ A client can automate retrying an idempotent request without risking an unintended change.
- ❌ Retrying a payment
POSTwithout an idempotency key can charge twice. - ❌ Retrying an "append event" or "create order" command without extra protection can duplicate it.
Worked example: retry a PUT whose response was lost
An API sets an account's notification channel through PUT /users/42/preferences. The initial state is sms, and the client wants to replace it with email. The server applies the first request, but the connection breaks before the client receives the response. The client only knows that the outcome is uncertain.
The retryable request keeps the same method, resource, and representation:
PUT /users/42/preferences HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Content-Length: 25
{"notifications":"email"}
The server's core logic behind the PUT assigns a known value. handledRequests represents an allowed incidental effect, such as a metric or log entry, and makes it visible that the server processed two requests:
type Channel = "email" | "sms";
let storedPreference: Channel = "sms";
let handledRequests = 0;
function putPreference(channel: Channel) {
handledRequests += 1;
storedPreference = channel;
return { storedPreference, handledRequests };
}
console.log(putPreference("email"));
console.log(putPreference("email"));
The expected output shows different handledRequests values but the same intended storedPreference state:
{ storedPreference: 'email', handledRequests: 1 }
{ storedPreference: 'email', handledRequests: 2 }
This partial Node.js 24 client uses the built-in fetch. With a URL and request known to be valid, the first rejection represents the scenario's uncertain communication failure. The second call runs inside the catch, but no additional try/catch protects it; if it also fails, the error propagates without a third automatic attempt:
const url = "http://localhost:3000/users/42/preferences";
const request: RequestInit = {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ notifications: "email" }),
};
const send = () => fetch(url, request);
let response: Response;
try {
response = await send();
} catch {
response = await send();
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
console.log(await response.json());
After the first PUT, the preference is email. After the identical retry, it is still email: no additional preference change was requested. If the operation instead intended to append a new item for every request, two identical requests would ask for two items, and that contract would not be idempotent. In production, the client must also classify retryable communication errors and respect authorization, preconditions, and concurrency.
Applying the rule beyond the example
Start with the method's contract. RFC 9110 defines PUT, DELETE, and the safe methods as idempotent. A safe method, like GET, is read-only. Idempotency is broader: an idempotent method can still change state. DELETE asks to remove something, but repeating the same request doesn't ask for another removal beyond the first.
If the connection fails before a response arrives, a client can automatically retry an idempotent request: the repeat preserves the intended effect. Still, idempotency doesn't guarantee the retry will succeed. Authorization can expire, a precondition can fail, or another client may have modified the resource in the meantime.
Don't assume idempotency from the endpoint's name or how harmless it looks. A payment, a notification, or an "add item" command isn't idempotent if repeating the request asks for another effect beyond the first. That's why many payment APIs add an Idempotency-Key: the client generates a unique key per operation and sends it with every attempt. The server stores it; if it sees the same key twice, it doesn't reprocess the payment and returns the original response instead. This is an application-level solution, not a guarantee from the HTTP method itself.
Before automatically retrying a request whose method isn't idempotent, you need to know that this specific request is idempotent (for example, via an Idempotency-Key), or have a way to detect that it was never applied. Otherwise, don't assume retrying is safe. A person can inspect the resource and decide to retry manually; that's different from automating it.
A client shouldn't automatically retry something that already failed on a previous automatic retry. An HTTP proxy shouldn't automatically retry a non-idempotent request either. These limits keep an uncertain failure from turning into an endless retry policy.
Use three questions when evaluating a retry policy:
- Are the repeated requests identical in target and intended semantics?
- Does repeating them request any intended server effect beyond one request?
- Can authorization, preconditions, concurrency, or application rules make an automatic retry inappropriate even though the method is idempotent?
These questions keep the guarantee narrow. Idempotency helps reason about repeated intent after uncertain communication; it does not replace the rest of the API's consistency, concurrency, or failure-handling design.
Evidence
- RFC 9110, Section 9.2.1: Safe Methods distinguishes read-only requested semantics from idempotency.
- RFC 9110, Section 9.2.2: Idempotent Methods defines idempotency in terms of the intended effect on the server and specifies when a client can automatically retry.
- RFC 9110, Section 9.3.4: PUT notes that a
PUTcan have effects on other resources while retaining its method semantics. - RFC 9110, Section 9.3.5: DELETE defines
DELETEin terms of removing the association between the target resource and its current functionality. - Node.js 24 documentation:
fetchdocuments the built-in HTTP client used by the TypeScript example.
Cited sources
- RFC 9110: HTTP Semantics, Section 9.2.2 (Primary, Jul 14, 2026)
- Node.js v24 API documentation, Fetch global (Official, Jul 18, 2026)