AI Tool Pipelines — Automate Your WorkflowsAI Tool Pipelines

Idempotency Keys for API Pipelines: How and Why

6 min read · Updated Aug 5, 2026

Detailed macro view of a motherboard with visible electronic components and connectors

An idempotency key is a unique identifier you generate once per logical operation (a client-generated UUID, usually) and send in a header on every attempt of that same request, including retries. The server checks whether it has already processed that key. If it has, it returns the original result instead of doing the work again. This is what makes it safe to blindly retry a payment, an order creation, or an outbound message. Without an idempotency key, retrying a mutating request risks doing it twice.

Key takeaways

  • Idempotency means calling an operation multiple times produces the same result as calling it once. A GET request is naturally idempotent, a POST that creates something is not, unless you add an idempotency key.
  • Generate the key once per logical operation on the client, a UUID is standard, and send it in the same header on every retry of that same operation.
  • The server stores a mapping of idempotency key to the result of the first successful attempt, and returns that stored result for any duplicate key instead of repeating the side effect.
  • Idempotency keys and exponential backoff solve different halves of the same problem: backoff decides when to retry, idempotency keys make it safe to retry a mutating request at all.
  • Expire stored idempotency keys after a reasonable window, 24 hours is a common default, rather than keeping them forever, or the storage grows unbounded.

Why retrying a POST is not automatically safe

A retry only knows the previous attempt failed on the client side, it has no idea whether the server actually finished the work before the connection dropped. If a charge-creation request times out after the charge was already created but before the response made it back, retrying that same request without an idempotency key creates a second charge. The client did nothing wrong, it followed exactly the retry logic it was supposed to. The gap is that "the request failed" and "the operation did not happen" are two different claims, and only one of them is guaranteed.

How the key actually works, server side

The server keeps a short-lived store, a Redis hash or a database table works fine, mapping each idempotency key it has seen to the response it returned the first time. Every incoming request checks that store before touching anything else.

javascript
async function idempotencyMiddleware(req, res, next) {
  const key = req.headers['idempotency-key'];
  if (!key) return res.status(400).send('Missing idempotency key');

  const cached = await store.get(key);
  if (cached) {
    if (cached.bodyHash !== hashBody(req.body)) {
      return res
        .status(422)
        .send('Idempotency key reused with a different request body');
    }
    return res.status(cached.status).json(cached.response);
  }

  const originalJson = res.json.bind(res);
  res.json = async (body) => {
    await store.set(
      key,
      { status: res.statusCode, response: body, bodyHash: hashBody(req.body) },
      { ttlSeconds: 86400 },
    );
    return originalJson(body);
  };

  next();
}
A minimal idempotency middleware, checking and storing results against the key before the handler ever mutates anything.

Where to generate the key: always the client, never the server

If the server generates the idempotency key, the client has no way to send the same key back on a retry, since the very request that would have returned that key is the one that timed out. The key has to originate on the client, generated once before the first attempt and reused, unchanged, on every retry of that same logical operation. This is the opposite of most ID generation in a system, where the server is usually the source of truth.

Close-up of ethernet cables plugged into a network switch, representing repeated requests hitting an API

The eleven duplicate charges in four minutes

In November 2021, a payments pipeline I worked on for a fintech client had exponential backoff wired up on every outbound call, but no idempotency key on the charge-creation request. A network blip during a routine deploy caused eleven duplicate charges against the same customer within about four minutes before anyone noticed, real money moved, and every one of those charges had to be refunded by hand the following morning along with an apology email. We added a client-generated idempotency key to that endpoint the same afternoon. The bug did not get rarer. It disappeared, because the retries kept happening exactly as before, they just stopped doing the work twice.

Close-up of colorful JavaScript code on a computer monitor implementing an idempotency check

Which methods actually need one

GET, PUT, and DELETE are naturally idempotent by HTTP spec convention. POST is not, and PATCH depends on what it does.
MethodNaturally idempotentNeeds a key
GETYesNo
PUT (full replace)Usually yesNo
DELETEUsually yesNo
POST (create)NoYes
PATCHDepends on the operationYes, if it increments or appends rather than sets

I think idempotency keys should be mandatory on any endpoint that moves money or sends a message, not optional hardening added after the first incident. The cost is one extra header and a small cache table. The cost of skipping it is a support queue full of "why was I charged twice," discovered by a customer before it is discovered by you.

Stripe’s idempotency key documentation (accessed 2025) states that idempotent request results are cached for 24 hours by default, after which the same key can be reused for a new operation. That window is a reasonable default for most pipelines: long enough to cover any realistic retry sequence, short enough that the storage does not grow forever.

“A retry without an idempotency key does not ask "did that work." It asks "let us find out again," on someone else’s money.”

Frequently asked questions

Frequently asked questions

What is an idempotency key?

A unique identifier, usually a client-generated UUID, sent on every attempt of the same logical operation. The server uses it to recognize a retry and return the original result instead of repeating the operation’s side effect.

Where should the idempotency key be generated?

On the client, before the first attempt, and reused unchanged on every retry of that same operation. If the server generates it, the client has no way to send it back on a retry, since the response containing it may never have arrived.

What happens if I reuse an idempotency key with a different request body?

A well-implemented server should reject it, typically with a 422 or 400 error, rather than silently returning the cached result for a different request. Reusing a key with a changed body usually indicates a bug in key generation, not a legitimate retry.

How long should a server remember an idempotency key?

Long enough to cover any realistic retry window for that operation. 24 hours is a common default (Stripe uses this), balancing coverage for delayed retries against unbounded storage growth.

Do GET requests need idempotency keys?

No. A GET request should not have side effects, so calling it multiple times is already safe by definition. Idempotency keys matter for requests that create, charge, or send something, most commonly POST.

Does idempotency replace retry and backoff logic?

No, they solve different halves of the same problem. Backoff decides when and how often to retry a failed request. An idempotency key makes it safe to actually perform that retry on a mutating operation without risking a duplicate side effect. Most production pipelines need both.