Skip to main content

Confirm

Are you sure?

Guide

Idempotency keys explained how not to pay twice for a retry

A request times out. Did the server get it? You can't tell — and retrying might do the work, and the charge, twice. An idempotency key is how you retry without guessing.

By · Last updated: September 2026

TL;DR

An idempotency key is a unique id the client generates for one logical request and sends in an Idempotency-Key header. If the client retries with the same key, the server returns the first request's result instead of doing the work again. Generate one key per operation, reuse it only for retries of that operation, and expect a server to refuse a key reused with a different body.

Free plan, no credit card. 1,000 credits a month.

The problem

A timeout doesn't tell you whether it worked

You send a request. The connection drops before the response arrives. Did the server receive it, do the work and charge you — or never see it? From the client, those look identical.

If the request only reads something, it doesn't matter: send it again. If it does something — takes a payment, sends an email, starts a paid job — retrying blindly can do it twice, and not retrying can mean it never happened. Neither is acceptable, and guessing is how duplicate charges and missing orders happen.

HTTP already classifies methods this way. GET, PUT and DELETE are idempotent by definition: sending one twice has the same effect as sending it once. POST isn't, and POST is what most APIs use for work. An idempotency key is how a POST gets the same guarantee.

The mechanism

How an idempotency key works

  1. 1
    The client generates a unique key for one logical operation — a UUID is ideal — and sends it in an Idempotency-Key header.
  2. 2
    The server looks the key up. If it's new, it records the key with a fingerprint of the request, does the work, and stores the response against the key.
  3. 3
    If the client retries with the same key, the server finds it and returns the stored response instead of doing the work again. One operation, one effect, one charge — however many times it was sent.
  4. 4
    After a retention window the key expires, and the same value would be treated as a new request.
The same key on the first attempt and on every retry
KEY=$(uuidgen)

curl -X POST https://api.example.com/charges \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 2500, "currency": "usd"}'
# …connection reset. Retry with the SAME key:
curl -X POST https://api.example.com/charges \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 2500, "currency": "usd"}'

Stripe popularized the header, and many APIs copied it. Its behaviour is described in an IETF Internet-Draft from the HTTPAPI working group — useful as a reference, though not a finished RFC — so check each API's own documentation for its exact rules.

The edge cases

What a careful server does with keys

SituationSensible response
Same key, same request, first one finishedReturn the stored response, and say it's a replay
Same key, same request, first one still runningWait for it and return its result, or answer 409 Conflict so the client retries later
Same key, different request bodyRefuse with an error — the key would otherwise stand for two different operations
First attempt failed before doing anythingLet the key be reused, so a later retry can succeed
Key past the retention windowTreat it as a brand-new request

The fingerprint matters more than it looks. Without it, a client bug that reuses a key for a different operation silently gets the first operation's response — a charge for $25 answered with the receipt for $40. Binding the key to the request and refusing mismatches turns that bug into an error you'll see.

Client side

How to use keys correctly

  • One key per operation, not per attempt. Generating a fresh key on each retry defeats the purpose — every retry becomes new work.
  • Create the key before the first attempt, and keep it. If the operation matters, store the key with the job or the order, so a retry after a crash or a redeploy still uses it.
  • Never reuse a key for new work. The next order, the next page, the next charge each gets its own.
  • Retry with backoff. A key makes retries safe; it doesn't make them free for the server. Wait longer between each.
  • Know the window. A retry that arrives after the key expired is a new request. For most APIs that's a day or more, which covers any sane retry policy.

Server side

Adding idempotency keys to your own API

If you run an API that does costly or irreversible work on POST, supporting the header is a table and a few rules. The one part that needs care is the race: two copies of the same request arriving at once must not both do the work, so the key has to be claimed atomically before anything runs.

One row per key, claimed with a unique constraint
CREATE TABLE idempotency_keys (
  account_id    bigint      NOT NULL,
  key           text        NOT NULL,
  fingerprint   text        NOT NULL,  -- hash of method, path and body
  status        text        NOT NULL,  -- 'running' or 'done'
  response_code integer,
  response_body jsonb,
  created_at    timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (account_id, key)
);

-- Claim the key. Zero rows inserted means someone already holds it.
INSERT INTO idempotency_keys (account_id, key, fingerprint, status)
VALUES ($1, $2, $3, 'running')
ON CONFLICT DO NOTHING;
  1. 1
    Insert first. If the insert succeeds, you own the key: do the work, then store the response and mark it done.
  2. 2
    If it conflicts, read the row. A different fingerprint is a client bug — refuse it. The same fingerprint and done means replay the stored response. The same fingerprint and running means wait for it, or answer 409.
  3. 3
    Release keys on failures that did nothing. If the request failed validation or couldn't start, delete the row so a corrected retry isn't stuck with the error.
  4. 4
    Expire old rows on a schedule — a day or two is plenty for retries.

Keys vs. caches

Why a cache doesn't replace an idempotency key

They answer different questions. A cache answers "do you have a fresh enough copy of this?" — it's about the data. A key answers "did I already ask for this?" — it's about the request.

The difference shows when a client asks for fresh data. A request that says "fetch this page now, don't serve me a stored copy" bypasses any cache, by design. If it times out and the client retries, a cache can't help — the retry also asks for a fresh fetch, and gets one, and pays for one. Only a key can tell the server that the retry is the same request, so it returns the first attempt's result.

With URLpipe

How URLpipe handles retries and keys

URLpipe charges per page, so it's built so a retry never pays twice. There are three layers, and two of them need nothing from you:

  • Cache hits are free. A request whose result is fresher than max_age (7 days by default) is served from storage at no cost — see caching.
  • Duplicates in flight share one run. An identical request that arrives while the first is still working waits for it, for free, without using one of your parallel slots.
  • Idempotency-Key covers the rest. Within 24 hours, a retry with the same key returns the first request's token and result — one charge, one webhook — even with max_age=0. Replays carry Idempotent-Replayed: true.

A key is any string of up to 255 printable ASCII characters without spaces, and belongs to one project. It's bound to the operation, URL, options, max_age, report_to and labels it first arrived with; reusing it for a different request is refused with idempotency_key_reused and nothing runs. A request refused for credits or concurrency never claims its key, so a later retry with it is a fresh attempt. Over MCP, the same thing is the idempotency_key argument. The retries docs have the full rules.

FAQ

Frequently asked questions

What is an idempotency key?
A unique value — usually a UUID — that the client sends with a request so the server can recognise a retry of it. The server stores the key with the first request's result and returns that result to any retry carrying the same key.
Which HTTP methods need an idempotency key?
POST, mostly. GET, PUT and DELETE are defined as idempotent by HTTP semantics, so repeating them is safe by design. POST is not, which is why APIs that take POST for work that costs money add the header.
How long are idempotency keys kept?
It depends on the API: 24 hours is common, and Stripe keeps them for at least 24 hours. After the window, the same key is treated as a new request.
What happens if I reuse a key with a different request?
A well-behaved server refuses it with an error rather than guessing, because one key cannot stand for two different requests. Generate a fresh key for every new operation.
Isn't a cache enough to avoid paying twice?
A cache answers "do you have a fresh enough copy of this?"; a key answers "did I already ask for this?". A request that insists on a fresh fetch skips the cache, but its retry should still return the first attempt's answer — only a key can guarantee that.

Put this into practice.

Each of the eight kinds of data URLpipe returns has a free, no-signup tool — try the ideas from this guide on a real page, then grab an API key to run them from your code. 1,000 credits a month, no card.