What idempotent means
An operation is idempotent if doing it twice leaves things exactly as doing it once would. Setting a job’s status to “checked” is idempotent: the second time changes nothing. Adding one to a counter is not: every repeat adds again.
HTTP gives the standard definition. RFC 9110 says a request method is idempotent “if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request.” PUT, DELETE and the safe methods such as GET are idempotent. POST is not, which is why submitting a payment form twice can charge twice.
The definition is about the intended effect, not every side effect. A server may still write a log line for each request. What matters is that the thing the client asked for happens once.
Why retries make it necessary
Every network has the same blind spot. A sender sends a request and hears nothing back. Either the request was lost, or it arrived, the work was done, and the reply was lost. The sender cannot tell which.
The safe reaction to silence is to try again, which is how at-least-once delivery works. But a retry after a lost reply delivers the same request a second time. RFC 9110 states the rule plainly: a client should not automatically retry a non-idempotent request unless it knows the request is safe to repeat or can detect that the first attempt was never applied.
So retries and idempotency come as a pair. Without retries, messages get lost. Without idempotency, retries cause double work.
Why offline delivery makes it harder
Offline and mesh systems retry more, and over longer gaps, than a phone talking to a server on a good connection.
- A device keeps unsent work in an outbox and resends it when a path returns, sometimes hours later.
- An app can be closed or restarted between attempts, and must resend anything it cannot prove arrived.
- In a mesh, a message can reach the same device by two routes, or be held and forwarded by relays using store-and-forward.
- Acknowledgments travel back across the same unreliable links and can be lost too.
A receiver in this world should expect to see the same operation more than once, possibly days apart, and possibly wrapped in a different network message each time.
Idempotency keys
When an operation is not naturally idempotent, the usual fix is an idempotency key: a unique value the client generates once per intended operation and sends with every attempt.
Stripe’s API is a well documented example. The server saves the status code and body of the first request made with a key, whether it succeeded or failed, and returns that same result for later requests with the same key. It compares the parameters of a repeat with the original and returns an error if they differ, which catches a key accidentally reused for a different operation. Keys can be up to 255 characters, Stripe suggests random V4 UUIDs, and it warns against putting personal data in them. Keys can be removed once they are at least 24 hours old, and a key reused after that is treated as a new request.
An IETF working group draft proposed a standard Idempotency-Key HTTP header along the same lines. The draft has since expired without becoming an RFC, but it sets out the common rules: the key must be unique and never reused with a different payload; a repeat that arrives after the original completed gets the original result; a repeat that arrives while the original is still running gets a conflict error; and the server should publish how long it keeps keys.
Designing idempotent operations
A few habits make offline delivery safe to retry.
Prefer setting state to changing it. “Set the stock count to this value” can be repeated; “subtract the parts used” cannot. Google Cloud Storage’s retry guide sorts its operations into always, conditionally and never idempotent, and makes some writes conditionally idempotent with preconditions: the write applies only if the object is still at the version the client expected.
Create the key with the operation, not with the message. The identifier has to survive app restarts and every resend. If a retry creates a new network message, it should still carry the original operation ID.
Record the result with the effect. Store the operation ID and its outcome in the same transaction as the change itself. Otherwise a crash between the two leaves the receiver unsure whether the work happened.
Keep keys longer than a retry can arrive. A key that is pruned before a delayed copy turns up is no protection. For devices that can be offline for days, that window is much longer than an online API needs.
Offline Protocol’s local handoff guide follows this pattern: the sending app gives each operation an ID that survives retries and restarts, and the receiving app looks the ID up, commits the operation and its acceptance result together, and returns the stored result if the same operation arrives again.