Idempotency keys in 2026: how to make REST API retries safe

How idempotency keys make POST and PATCH retries safe: the header, server implementation, the race-condition trap, storage, and security.

Read time
12 min
Word count
1.8K
Sections
13
FAQs
8
Share
Glowing looping data path on a dark circuit board representing safe API retries
Idempotency keys let a client retry a POST without doubling the operation.
On this page · 13 sections
  1. Which methods need a key
  2. Why a retry is dangerous
  3. The Idempotency-Key header
  4. Implementing it on the server
  5. The race condition most tutorials miss
  6. Storage and TTL
  7. Compare the request, not just the key
  8. Security considerations
  9. Common failure modes
  10. India-specific considerations
  11. FAQ
  12. How eCorpIT can help
  13. References

Summary. A payment POST times out after 2 seconds, the client shows an error, the user taps "pay" again, and now a $20.00 charge has hit the card twice. Idempotency keys stop exactly that. The client sends a unique value in an Idempotency-Key header, the server stores the first response against that key, and every retry returns the stored response instead of running the operation again. Stripe and Adyen have offered this for years, Stripe expires keys after 24 hours, and the pattern is now an IETF Internet-Draft, "The Idempotency-Key HTTP Header Field," whose draft 07 carried an expiry of 18 April 2026. This guide shows how the header works, how to implement it on the server without a race condition, how long to keep keys, and the security traps to avoid. In India, where a double-charged ₹2,000 order means a support ticket and a refund, this is not optional plumbing.

As API author Phil Sturgeon puts it: "Idempotency is when doing an operation multiple times is guaranteed to have the same effect as doing it just once." Some HTTP methods already have this property. GET, HEAD, PUT, DELETE, OPTIONS, and TRACE are defined as idempotent, so repeating DELETE /users/123 leaves user 123 deleted either way. POST and PATCH are the problem. They exist to cause state changes, and sometimes you genuinely want the same request to happen twice, so the server cannot simply block duplicates. The idempotency key is how the client tells the server which case this is.

Which methods need a key

The header only matters for the non-idempotent methods. This table shows where a key earns its keep.

Method Idempotent by default? Needs a key for safe retry?
GET Yes No
PUT Yes No
DELETE Yes No
POST No Yes
PATCH Not guaranteed Often yes

PATCH is the subtle one. Many PATCH requests are idempotent by design, but the HTTP specification does not require it. A JSON Patch operation like incrementBy: 1 is clearly not idempotent, because sending it twice increments the value twice. When a PATCH can change state cumulatively, treat it like POST.

Why a retry is dangerous

Picture a payment client with a 2-second timeout:


            await axios.post(
  '/payments',
  { to: 'user@example', value: 2000 },
  { timeout: 2000 }
);
          

If the call does not finish in 2 seconds, the HTTP client reports a failure and offers a retry. The trouble is that the client does not know whether the server completed the payment or was merely slow to return the JSON. The server, meanwhile, does not know the client gave up. Without more information, a second POST /payments is indistinguishable from a legitimate second payment. As Sturgeon frames it, if the server receives the same charge twice, "it could be a failed retry, or it could just as easily be somebody paying Julian for a second cider." The key resolves the ambiguity.

The Idempotency-Key header

The client generates a unique value and sends it in the Idempotency-Key header. Here is the Stripe form, using curl:


            curl https://api.stripe.com/v1/charges \
  --user sk_test_xxx: \
  --header "Idempotency-Key: uWeBuDsZPxxvdhND" \
  --data amount=2000 \
  --data currency=usd \
  --data source=tok_mastercard
          

Any later request with the same key does not create a new charge; the server returns the saved response from the first call. Stripe recommends V4 UUIDs or another random string with enough entropy, limits keys to 255 characters, and warns against putting sensitive data such as email addresses in the key. Sending the same key with different parameters returns an error, because the new request does not match the original.

This is now standardised work. The IETF's HTTPAPI working group, the same group behind RFC 9457 problem details, has an Internet-Draft called "The Idempotency-Key HTTP Header Field." It is documented on MDN and increasingly implemented, but it remains a draft rather than a finished RFC, so treat the fine print as still moving.

Implementing it on the server

Start with a plain POST endpoint in Express that does no deduplication:


            app.post("/things", (req, res) =>
  res.json({ message: `New random number: ${Math.random() * 100}` })
);
          

Every call returns a different number, so a client cannot safely retry. Add middleware to check for a key and short-circuit on a repeat. Sturgeon uses the express-idempotency package:


            const idempotency = require("express-idempotency");
app.use(idempotency.idempotency());

app.post("/things", (req, res) => {
  const service = idempotency.getSharedIdempotencyService();
  if (service.isHit(req)) return; // repeat: stored response already sent

  res.json({ message: `New random number: ${Math.random() * 100}` });
});
          

Now three calls with no key each run the operation, which is the correct behaviour for existing clients, but three calls carrying Idempotency-Key: cheesecake all return the identical response. On the client, generate the key when the user starts an operation:


            const { v4: uuidv4 } = require('uuid');
const idempotencyKey = uuidv4(); // set when the form loads

fetch("https://example.org/api/things", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey,
  },
  body: JSON.stringify(payload),
});
          

Key lifetime is a UX decision. If the user expects a new operation on every submit, reset the key after each success. If they are retrying one operation, keep the same key throughout.

The race condition most tutorials miss

The in-memory demo above breaks in production for two reasons. First, several servers behind a load balancer each hold their own local state, so a key seen by one is unknown to another. Second, an in-memory store is wiped on every deploy. You need shared, durable storage.

There is a subtler bug too. The naive "check if the key exists, then act" pattern has a race: two concurrent requests with the same key can both pass the existence check before either writes, and both run the side effect. The fix is to make a database INSERT with a UNIQUE constraint on the key the very first operation. The database detects the conflict atomically, so exactly one request wins. On a unique-constraint violation, you return the stored response instead of processing again.

A workable idempotency records table looks like this:


            CREATE TABLE idempotency_records (
  idempotency_key  TEXT PRIMARY KEY,
  request_hash     TEXT NOT NULL,
  response_status  INT,
  response_body    JSONB,
  created_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
  expires_at       TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_idempotency_expires ON idempotency_records (expires_at);
          

Insert the key first, inside the same transaction as the business write where possible, then store the status and body once the work succeeds. The index on expires_at lets a cleanup job remove stale rows cheaply.

Storage and TTL

Where you store keys is a trade-off between speed and durability.

Approach Durability Best for
In-memory None; lost on restart Local demos only
Redis with TTL Configurable, fast High-volume checks outside the business transaction
Relational DB, unique constraint Strong, transactional Payments and orders that must be exactly-once

The TTL rule is simple: the record must outlive the client's retry window. If a provider retries for 48 hours, the deduplication data has to persist at least 48 hours. Stripe expires keys after 24 hours. Keep the window measured in hours or days, never years, both to limit storage growth and to reduce the reuse risk covered below.

Compare the request, not just the key

Storing a request_hash is not optional detail; it is a correctness and security control. Before you reuse a stored response, compare the new request against the original by hashing the body alongside the key. This does two things. It catches the honest bug where a client reuses a key with a changed payload, which should return a mismatch error rather than the wrong saved response. And it makes it far harder for one party to reuse a known key without also knowing the full original request. Libraries such as express-idempotency do this comparison automatically.

Security considerations

A reused key returns the stored response regardless of who sends it. If two different clients send Idempotency-Key: same-thing, the second could receive the first client's response. That can leak data directly, or leak the mere existence of an operation that should be private, such as whether one user ever paid another. Sturgeon lists the defences, and they are worth following exactly.

Use sufficiently random values such as UUIDs, so independent clients effectively never collide. Expire keys after a short time, hours or days, so they cannot be replayed indefinitely. Compare the request body before reusing a response. Run authentication and rate limiting before the idempotency check, so the deduplication layer cannot be brute-forced anonymously. And treat keys as best-effort: for an irreversible, high-value action, do not rely on an idempotency key as the only safeguard; check explicitly whether the operation already happened before retrying after a long gap. When a key mismatch or conflict occurs, return the error in a structured format like RFC 9457 problem details so clients can react programmatically.

Common failure modes

Idempotency keys are harder than the header makes them look, and a 2026 survey of production incidents keeps surfacing the same handful of mistakes. Knowing them upfront saves a painful debugging session.

Storing the response too early is the first. If you write the stored response before the business transaction commits, a crash between the two leaves a key that maps to a success the system never actually performed, and the retry returns a phantom result. Insert the key first, but write the response only after the real work commits, ideally in the same transaction.

Setting the TTL shorter than the retry window is the second. A cache that expires after 6 hours cannot protect a client that retries after 12, and the operation runs twice. Match the record's lifetime to the longest realistic retry, then add a margin.

Skipping the request fingerprint is the third. Without hashing the body, a client that reuses a key with a new payload silently gets the old response, and the two operations quietly merge. A ₹500 top-up and a ₹5,000 top-up sharing a key is a bug you never want to explain to a customer.

Relying on the key as the sole guarantee for irreversible actions is the fourth. Keys are best-effort. For a wire transfer or an account deletion, add an independent check that the operation has not already run, rather than trusting the key alone.

The fifth is treating in-memory state as durable across a fleet. Behind a load balancer, or across a deploy, local state disappears, so a shared store is mandatory the moment you run more than one instance.

India-specific considerations

For teams shipping payments, quick-commerce, or D2C checkout flows in India, idempotency is a direct line to fewer refunds and support tickets. A retried UPI or card POST that double-charges a ₹2,000 order costs real money and trust to unwind. Two India-flavoured notes. First, the Idempotency-Key and stored request must not contain personal data, which keeps the deduplication store clean under the DPDP Act. Second, on flaky mobile connectivity, retries are frequent, so the payoff from correct idempotency is larger than in a data-centre-to-data-centre setting. Build it into the API contract from day one rather than retrofitting after the first double-charge incident. If you are consolidating services, do it as part of a wider monolith-to-microservices modernization so every service shares one idempotency module.

FAQ

How eCorpIT can help

eCorpIT builds and hardens payment and checkout APIs for clients in India and abroad, and safe-retry handling is part of that work. If your team wants an idempotency layer designed for exactly-once payments, a review of your current retry behaviour, or a shared module across a set of microservices, our senior engineering team can scope it with you. Reach us through the contact page.

References

  1. Phil Sturgeon, Working with the new Idempotency Keys RFC, HTTP Toolkit blog, December 2023.
  1. Stripe, Designing predictable APIs with idempotency, Stripe engineering blog.
  1. Stripe, Idempotent requests, API reference, docs.stripe.com.
  1. IETF, The Idempotency-Key HTTP Header Field (Internet-Draft), datatracker.ietf.org.
  1. MDN, Idempotency-Key header, developer.mozilla.org.
  1. Brandur Leach, Implementing Stripe-like Idempotency Keys in Postgres, brandur.org.
  1. Zuplo, Implementing Idempotency Keys in REST APIs, zuplo.com.
  1. OneUptime, How to Handle Idempotency in Microservices, oneuptime.com, January 2026.
  1. Adyen, API idempotency, docs.adyen.com.
  1. IETF HTTPAPI working group, idempotency repository, GitHub.
  1. Java Code Geeks, Idempotency Keys Are Harder Than They Look, June 2026.
  1. Luca Palmieri, An In-Depth Introduction To Idempotency, lpalmieri.com.

_Last updated: July 7, 2026._

Frequently asked

Quick answers.

01 What is an idempotency key?
An idempotency key is a unique, client-generated value sent in the Idempotency-Key HTTP header so a POST or PATCH request can be retried safely. The server stores the first response against the key and returns that same response for any repeat, so a retried payment or order is not processed twice.
02 Why do POST requests need idempotency keys?
GET, PUT, and DELETE are idempotent by definition, so repeating them is safe. POST and PATCH are not: a retried POST can create a second order or charge a card twice. If a request times out, the client cannot tell whether the server finished. An idempotency key removes that ambiguity.
03 Is the Idempotency-Key header a standard?
It is a widely used convention, popularised by Stripe and Adyen, and now an IETF Internet-Draft, "The Idempotency-Key HTTP Header Field," in the HTTPAPI working group. Draft 07 carried an expiry of 18 April 2026. It is not yet a published RFC, so the exact details can still change.
04 How should the server store idempotency keys?
Store each key with the request fingerprint, the response status, and the response body, plus a created and expiry timestamp. Use a relational database with a unique constraint on the key for durability and transactional safety, or Redis with a TTL for fast lookups when the check need not share the business transaction.
05 How do I avoid a race condition with idempotency keys?
The naive check-then-act pattern lets two concurrent requests with the same key both pass the existence check. Instead, make a database INSERT with a unique constraint on the key the first operation. The database rejects the duplicate atomically, and you return the stored response instead of running the side effect twice.
06 What TTL should an idempotency key have?
The key must live at least as long as the client's retry window. If a provider retries for 48 hours, the deduplication record must persist at least that long. Stripe expires keys after 24 hours. Keep the window in hours or days, not years, to limit reuse risk and storage growth.
07 What are the security risks of idempotency keys?
A reused key returns the stored response regardless of who sends it, so a guessable key could leak one client's response to another. Mitigate this with random UUID keys, short expiry, and by comparing the new request against the stored one before reusing a response. Run authentication before the idempotency check.
08 When should the client change the idempotency key?
Generate a new key when the user starts a fresh operation, for example when a form loads, and keep the same key while retrying that one operation. If you reuse a key for what should be separate actions, they collapse into one; if you rotate it mid-retry, you lose the safety it provides.

About the author

Manu Shukla

Founder & Director

Founder of eCorpIT. Hands-on engineer leading senior-only delivery for AI apps, custom software, and cloud systems for global clients.

Subscribe

One engineering note a week. No fluff, no spam.

Senior-architect playbooks on AI agents, mobile apps, cloud, security, data, and marketing — delivered every Wednesday.

Past the reading

Read enough. Let's build something.

A senior architect responds in 24 working hours with scope, indicative cost, and a timeline. NDA before any technical conversation.