Idempotency Keys: How APIs Survive an Unreliable Network


Every API call travels over a network that makes no promises. A request can time out after the server already processed it. A response can get lost on the way back even though the underlying operation succeeded. A client, seeing no response, does the only reasonable thing: it retries. That’s where idempotency keys come in, and why nearly every payments API, and a growing number of general-purpose APIs, now treat them as a first-class concept.

The Problem Retries Create

Retrying a failed request sounds harmless until the request wasn’t actually a read, it was a write. “Charge this card $50” or “create this order” is not safe to blindly resend. If the first attempt succeeded on the server but the client never got confirmation, a naive retry produces a duplicate charge or a duplicate order. The client has no way to distinguish “my request never arrived” from “my request arrived and worked, but the response didn’t make it back.”

This is a fundamental limit of networks, not a bug in any particular implementation. TCP can guarantee delivery of bytes on an open connection, but it can’t guarantee that an application-level request-response cycle completes cleanly across timeouts, proxies, load balancer failovers, and client crashes. Something above the transport layer has to take responsibility for making retries safe.

The Idempotency Key Pattern

The common solution is simple to state: the client generates a unique key, usually a UUID, and attaches it to the request, typically as a header like Idempotency-Key. The server stores the key alongside the result of processing that request. If the same key shows up again, the server doesn’t reprocess the operation. It just returns the stored result from the first attempt.

This turns a non-idempotent operation, like “create a charge,” into an idempotent one from the client’s perspective. Retrying with the same key is now safe no matter how many times it happens, because the server treats repeat keys as “give me what happened last time,” not “do it again.”

The client’s job is to generate the key once per logical operation and reuse it on every retry of that operation. A fresh key on every retry defeats the entire purpose, since the server would treat each attempt as a brand new request.

Where the Real Difficulty Lives

The idea is easy to describe and genuinely hard to implement correctly. A few problems show up repeatedly:

Concurrent requests with the same key. If two requests with the same idempotency key arrive close together, before the first has finished processing, the server needs to make the second one wait or reject it, rather than let both proceed and double-execute the operation. This usually means locking or a status field, not a simple key-value cache.

Key storage and expiry. Idempotency records can’t live forever, but they also can’t expire too soon, or a legitimate delayed retry after a long outage will be treated as new. Most implementations keep records for a bounded window, often 24 hours, long enough to cover realistic retry scenarios.

Matching the request, not just the key. A key by itself isn’t enough. If a client reuses a key but sends a different request body, the server has to decide whether that’s an error or whether to serve the cached result anyway. Most well-designed APIs hash the request payload and reject key reuse with mismatched content, to catch client bugs early.

Partial failures inside the operation. An operation that touches multiple downstream systems, like charging a card and then writing an order record, needs the idempotency layer to also handle the case where the first step succeeded but the second didn’t. This often means the operation itself needs to be designed with its own recovery logic, not just wrapped in an idempotency check.

Why It’s Becoming Standard Practice

As more of the software world runs on distributed systems, HTTP APIs, and services calling other services across networks, the “retry might duplicate a write” problem shows up everywhere, not just payments. Idempotency keys are a pragmatic answer because they push the complexity to a single, well-understood place: the boundary of the API, where the client and server explicitly agree on how to handle repetition. That’s a much better outcome than every team inventing its own ad hoc deduplication logic, or worse, not handling it at all and finding out in production when a customer gets charged twice.