Skip to main content
All Posts
2026AI API vs Traditional API: What's Actually DifferentBuilding LLM-Backed APIs: A Practical Architecture GuideEmbedding APIs Explained: How They Work and When to Use ThemCost Optimization for LLM APIs: Caching, Routing, and Model SelectionLLM Rate Limiting: Token Buckets and Concurrency ControlStreaming LLM Responses: Patterns and PitfallsOpenAI API vs Anthropic API vs Gemini API: Production Trade-offsOpen vs Proprietary LLM APIs: Trade-offs for ProductionRAG APIs: Architecting Retrieval-Augmented Generation EndpointsVertical AI API Strategy: Building Domain-Specific AI APIsSmarter AI Traffic with Jev: How WSO2 AI Gateway Mediates LLM and MCP CallsAPI Analytics: The Metrics That Actually Matter (2026 Guide)How to Build a CI/CD Pipeline for APIsAPI Design Tools: Choosing the Right Stack for API-First TeamsAPI Gateway vs Load Balancer vs Reverse Proxy: Clearing the ConfusionAPI Management Platforms: Cloud vs. Self-Hosted (2026)API Management Tools: A Complete Buyer's Guide for 2026Cloud Native API Gateway: Architecture Guide for 2026Envoy vs NGINX: Which Proxy Fits Your API Gateway?Internal Developer Portal: What It Is and How to Build OneKong vs Tyk: Open Source API Gateway ShowdownKubernetes API Gateway: The 2026 Guide to Gateway APIMuleSoft vs WSO2: Enterprise API Management ComparedOpen Source API Gateways Compared: Kong, Tyk, Gravitee, WSO2What Is an API Gateway? Functions, Architecture & Use CasesAPI Architecture: Patterns, Principles & Modern ApproachesAPI Caching Strategies: Edge, Gateway, and Application LayersAPI Federation: Patterns for Multi-Team API ProgramsAPI-First Architecture: Why Design Before Code WinsAPI Gateway Architecture: How API Gateways Actually WorkAPI Security Architecture: Layered Defense for Modern APIsCircuit Breakers and Resilience Patterns for APIsAPI Architecture Diagrams: From L0 to Ln (Complete Guide)Backend-for-Frontend (BFF) Pattern: When and WhyEvent-Driven APIs: Webhooks, WebSockets, and SSEMicroservices vs Monolithic: Trade-offs and When to ChooseService Mesh vs API Gateway: When You Need Each (or Both)API Authentication Best Practices: OAuth, Keys, mTLS, JWTAPI Error Handling: Best Practices for REST APIsAPI Documentation Best Practices: Beyond Reference to Developer ExperienceAPI Rate Limiting: How It Works and Best PracticesAPI Pagination: Design Patterns & Best PracticesAPI Versioning Best Practices: Strategies for Long-Lived APIsGraphQL vs REST: Key Differences & When to Use EachHTTP Status Codes for API Design: A Practical GuideIdempotency Keys for APIs: The Complete GuideJWT vs OAuth: What's the Difference and When to Use EachOAuth 2.1: What's New and How It Differs from OAuth 2.0REST API Design: A Complete Guide for 2026What Is a REST API? A Plain-English GuideAI Agent API Integration: Patterns & Best PracticesEnterprise MCP: The Control Plane for AI AgentsMCP Gateway vs MCP Proxy: What's the Difference?MCP Governance: Controlling Third-Party MCP ToolsMCP Monitoring: Observability for MCP ServersMCP Security: Risks, Threats and ControlsMCP Server Security: 8 Best PracticesMCP Server Catalog: Building an Internal RegistryMCP Tool Poisoning: How It Works & How to Stop ItWhat Is an MCP Gateway? Features & BenefitsWhat Is an AI Gateway? Features, Benefits and How It WorksAI Gateway Comparison: Top Solutions in 2026AI Gateway Security: Guardrails for LLM TrafficAI Gateway vs API Gateway: Key DifferencesBest LiteLLM Alternatives in 2026AI Gateway Observability: Key Metrics, Logging, and Tracing for LLM TrafficLLM Cost Control: Strategies to Cut AI SpendLLM Routing: How It Works, Strategies, and Why It MattersPrompt Injection Defense: Techniques That Actually Work7 Billion Calls a Day: One WSO2 GatewayLLM Fallback: How to Build Resilient AI ApplicationsREST API to MCP Server: A Practical GuideAzure API Management vs AWS API Gateway: Key Differences and FeaturesMuleSoft vs AWS API Gateway: Key Differences and FeaturesMuleSoft vs Azure API Management: Key Differences and FeaturesApigee vs AWS API Gateway: Key Differences and FeaturesApigee vs Azure API Management: Key Differences and FeaturesApigee vs MuleSoft: Key Differences and FeaturesGravitee vs AWS API Gateway: Key Differences and FeaturesGravitee vs Azure API Management: Key Differences and FeaturesGravitee vs MuleSoft: Key Differences and FeaturesGravitee vs Apigee: Key Differences and FeaturesKong vs AWS API Gateway: Key Differences and FeaturesKong vs Azure API Management: Key Differences and FeaturesKong vs MuleSoft: Key Differences and FeaturesKong vs Apigee: Key Differences and FeaturesKong vs Gravitee: Key Differences and FeaturesTop 6 AWS API Gateway Alternatives of 2026Top 6 Azure API Management Alternatives of 2026Top 6 MuleSoft Alternatives of 2026Top 10 Apigee Alternatives of 2026Top 4 Gravitee Alternatives of 2026Top 6 Kong Alternatives of 2026

Idempotency Keys for APIs: The Complete Guide

· 17 min read
Technical Writer, WSO2

Every API client eventually runs into the same failure: a request goes out, the connection drops before a response comes back, and the caller has no idea whether the operation actually happened on the server. Retry, and you might charge a customer twice or create a duplicate order. Don't retry, and you might leave a legitimate request unfulfilled.

Idempotency keys exist to close that gap, and they've become one of the most practical, low-ceremony patterns covered in our API fundamentals hub.

What Is an Idempotency Key?​

An idempotency key is a unique value, generated by the client and sent with a request, that lets the server recognize when it has already processed that exact operation. If the same key arrives again, the server skips re-execution and returns the original response instead. The key is usually carried in an Idempotency-Key request header on POST or PATCH calls, the two HTTP methods that aren't idempotent by default.

In practice, this means a client can safely resend a request it's unsure about, with no risk of the server performing the underlying action more than once.

Why Idempotency Matters for APIs​

Networks fail in ways the client can't see. A request can succeed on the server while its response is lost in transit, a load balancer can time out a connection mid-flight, or a mobile client can lose signal while waiting for a reply. Each time, the client is left holding an unanswered request and a decision: retry, or don't.

Here's a concrete version. A client sends a request, and the server receives it, processes it, and commits the change, but the connection drops before the response gets back. From the client's side, this looks exactly the same as the server never receiving the request at all. The timeout says nothing about which of the two happened.

Without an idempotency mechanism, neither choice is safe. Retrying a payment, an order, or an account signup can produce a duplicate charge, order, or account. Not retrying risks silently dropping a request the server never received.

Idempotency keys narrow that gap, but the guarantee they provide is bounded, not absolute. As long as the client reuses the same key and the server still holds a record of it, a retry replays the original result instead of executing again. That only holds while the record exists. Once a key expires or is evicted from storage, a reused key is treated as a brand-new request, so "at most once" really means "at most once, within the retention window."

Idempotent vs. Non-Idempotent HTTP Methods​

HTTP already defines idempotency for some methods, and it's worth being precise about what that means before adding a key to the mix. GET, HEAD, PUT, DELETE, and OPTIONS are idempotent by specification: sending the same request several times should leave the server in the same state as sending it once. A PUT that sets a resource's name to "Invoice-042" produces the same end state whether it's sent once or five times.

POST and PATCH don't have that guarantee by default. RFC 5789, which defines the PATCH method, says plainly that PATCH is neither safe nor idempotent. A POST to /orders creates a new resource on every call, so five identical requests can create five separate orders. A PATCH that increments a counter or appends to a list changes the result every time it's applied, even when the request body stays the same.

The same RFC notes that a PATCH request can be designed to be idempotent. A "set this field to this value" patch behaves that way naturally, so the guarantee depends on how the operation is defined, not just on the method name. Idempotency keys cover the general case: they bring POST and PATCH into an idempotent-like state without changing what those methods mean or requiring every endpoint to be idempotent by design.

How Idempotency Keys Work​

The mechanics are the same across most implementations, even though header names and storage details vary:

  1. The client generates a unique key, usually a UUID, and attaches it to the request.
  2. The server checks whether it has seen that key before.
  3. If it hasn't, the server processes the request normally and stores the key alongside the response.
  4. If it has, the server skips re-execution and returns the stored response, as though the request had just been handled.

The Idempotency-Key Header​

An Idempotency-Key request header has become a de facto convention across API providers, and MDN documents it as a way to make POST and PATCH requests idempotent.

It's worth being clear about its status. MDN marks the header as experimental and explicitly non-standard, and its specification, the IETF Internet-Draft "The Idempotency-Key HTTP Header Field", has expired at the IETF rather than progressing to a finished RFC. That hasn't stopped it from becoming a widely adopted server-side convention, because it's an ordinary application-level header, not a browser feature. It isn't on the Fetch standard's forbidden-header list, so any client can attach it to a request, whatever a given browser implements natively.

The real browser-side consideration is CORS. Idempotency-Key isn't one of the small set of CORS-safelisted request headers, so a cross-origin call that carries it triggers a preflight OPTIONS request, and the server has to allow it explicitly through Access-Control-Allow-Headers. For API design, the pattern matters more than the exact header name a particular provider chooses.

According to the draft, the server is expected to check both the key and, optionally, an "idempotency fingerprint," a hash of the request itself, so that reusing a key with a different payload is treated as an error rather than silently accepted. The draft also defines the errors a server should return:

  • 400 if a required key is missing.
  • 409 if a request with the same key is still being processed.
  • 422 if the key has been reused with a payload that doesn't match the original.

None of that depends on the draft being an active or ratified standard. It's simply the convention most implementations already follow.

A retried payment request: the first attempt succeeds but its response is lost, and the retry with the same Idempotency-Key is replayed from the idempotency store without charging again

Server-Side Storage and Expiration​

Storing idempotency keys is more a caching problem than anything else. A common pattern is a key-value store, often Redis or a dedicated database table, keyed on the idempotency key and holding the request fingerprint, the response status code and body, and a timestamp.

Because the whole point is to protect against retries that happen shortly after the original request, most implementations expire keys after a bounded window instead of keeping them forever. A 24-hour retention window is common among public API providers. It's long enough to cover realistic retry scenarios without piling up an unbounded amount of historical response data.

Implementing Idempotency Keys: A Practical Walkthrough​

A naive implementation checks whether a key exists and only afterwards writes a record for it. That ordering has a race condition built in: two identical requests arriving close together can both pass the "does this key exist?" check before either has written anything, and both go on to execute.

The fix is to claim the key atomically, with an insert that succeeds once and fails for every later request, before the operation runs at all:

on POST or PATCH request:
caller = authenticate_and_authorize(request)
key = request.headers["Idempotency-Key"]
if key is required and missing:
return 400

scope = (caller.tenant_id, caller.id, operation_name, key)
fingerprint = hash(request.body)

claim = store.try_insert(scope, fingerprint, status="processing")
if claim.already_existed:
if claim.fingerprint != fingerprint:
return 422 # same key, different request body
if claim.status == "processing":
return 409 # original request is still in flight
return claim.response # replay the stored result for this same caller

response = process(request)
store.complete(scope, status="completed", response=response)
return response

Two things in that sketch matter beyond the happy path.

First, the scope includes the caller and tenant, not just the key. An idempotency key that isn't scoped to whoever created it can let a guessed or reused key retrieve someone else's cached response. That's why the sketch authenticates and authorizes the caller before doing anything with the key, so a stored result can only ever be replayed to the caller it belongs to.

Second, try_insert is doing real work. On a relational database, it's usually a unique constraint on (tenant_id, caller_id, operation_name, idempotency_key), used with an insert that either creates the row or reports a conflict without creating a duplicate. In Postgres, that looks like this:

INSERT INTO idempotency_records
(tenant_id, caller_id, operation_name, idempotency_key, fingerprint, status)
VALUES ($1, $2, $3, $4, $5, 'processing')
ON CONFLICT (tenant_id, caller_id, operation_name, idempotency_key) DO NOTHING
RETURNING id;

If a row comes back, this request owns the operation and should run it. If no row comes back, another request has already claimed the key, and the code above decides what to do based on that record's status and fingerprint.

That handles the race between two requests. It doesn't, on its own, handle a system failure between executing the side effect and saving its record. If the business mutation is a write to the same database, run it and the store.complete call in the same transaction as the claim, so they either both commit or both roll back. That matches the atomicity guidance Amazon's engineering team gives in its Builders' Library article on idempotent retries.

If the mutation instead calls an external system, such as a payment processor, a local transaction can't cover the remote effect. The process can crash after the payment succeeds but before the local record is written, and no amount of database atomicity fixes that, because the side effect has already happened outside the database. That case needs the same idempotency key passed on to the downstream system (most payment APIs accept one for exactly this reason), plus a reconciliation job that can detect and resolve the mismatch. An "at most once" claim can't rest on a transient local cache alone.

What the server does with an incoming key: an atomic claim, then 201 when the claim is won, 200 replay when the fingerprint matches, 409 while still processing, and 422 when the body differs

Handling Concurrent Requests with the Same Key​

The 409 Conflict response in the sketch is what a second request receives when it arrives while the first is still being processed. It tells the client to wait and retry, rather than assume failure and generate a new key. It's a direct result of claiming the key before running the operation instead of after: a request that loses the race finds a row that already exists, in a processing state it can't safely act on.

Choosing a Good Idempotency Key​

The key should be generated by the client, not the server, since the whole point is that the client controls retries. A version 4 UUID is the standard choice: it has enough entropy that accidental collisions between unrelated requests are effectively impossible, and it needs no coordination between clients. Avoid keys derived from predictable or reused values, such as a fixed string per endpoint, because that makes unrelated requests collide on the same key and defeats the purpose.

Common Implementation Mistakes​

A handful of mistakes account for most of the idempotency bugs seen in production APIs:

  • Reusing a key across different request bodies. If a client resends a key with a changed payload, the server needs to detect the mismatch and reject the request, rather than silently returning a stale response for a different operation.
  • No expiration policy. Storing every key forever turns a small cache into an unbounded table. Set a TTL that comfortably covers realistic retry windows, and let the store evict anything older.
  • Treating idempotency as a substitute for error handling. A key prevents duplicate execution. It does nothing for a request that was malformed, unauthorized, or invalid to begin with, which still needs its own validation and error responses.
  • Skipping atomicity in the check-and-store step. This reopens the race condition described above.
  • Assuming a database transaction covers a remote side effect. A transaction around the idempotency record and a local write only guarantees at-most-once execution for that local change. If the operation also calls an external system, a crash between that call succeeding and the local record being written can't be fixed with a bigger transaction, because the remote effect has already happened outside it.
  • Leaving the key unscoped to the caller. Without scoping a record to the caller or tenant that created it, a guessed or reused key can return someone else's cached response. Authenticate the caller before replaying a stored result, not just on the original request.

Idempotency Keys in Practice: Real-World Examples​

Payments are the classic use case, because a duplicate charge is a direct, measurable cost to a customer, not just an inconvenience.

Stripe's documented idempotency layer saves the status code and body of the first request made with a given key, including error responses, and returns that same result for every later request with the key. The guarantee is bounded, like any retention-based cache: Stripe removes keys automatically once they're at least 24 hours old, and a key reused after the original has been pruned simply starts a new request instead of replaying anything. Stripe also doesn't save a result if the incoming request fails validation or collides with another request for the same key that's still running. Since no endpoint actually started executing in either case, those requests can simply be retried. Stripe recommends generating keys as UUIDs, in line with the general guidance above.

If a browser-based client calls your API through WSO2 API Platform, the gateway is also where the CORS preflight issue mentioned earlier gets handled. The platform's CORS policy lets you list Idempotency-Key in Access-Control-Allow-Headers, so cross-origin retries carrying the key aren't rejected before they reach the backend. The deduplication itself still belongs in the service that owns the write, as described above.

Idempotency Keys for AI Agents and Automated Retries​

Idempotency keys were designed with a particular failure in mind: a human-driven client, or code written by a human, retrying after an ambiguous network error. AI agents introduce a different version of the same problem, at a different scale.

An agent calling an API to complete a multi-step task may retry a slow-to-confirm write as a matter of course, just as it would retry a read, unless the orchestration layer around it is specifically built to prevent that. WSO2's Nuwan Dias, VP and Deputy CTO, makes this point directly in a post on API design for AI agents: an agent that retries a slow-to-confirm write without an idempotency safeguard can produce exactly the duplicate charge that idempotency keys were built to prevent.

The server-side mechanism doesn't change. Something still generates a unique key for each intended write, stores it alongside the request, and returns the cached result if the same key arrives again. What has to change is upstream, in how reliably the calling layer holds on to that key.

The key needs to be persisted wherever the retry loop actually lives, whether that's the agent's own state or a tool-calling layer in front of it, and reused for every retry or resumption of the same logical write. Generating a fresh UUID each time the model repeats a tool call defeats deduplication entirely: the server sees what looks like a new request every time and processes it again. That's a bug in the orchestration layer, not a limitation of the idempotency-key pattern. It applies whether the agent calls a REST endpoint directly or goes through a tool-calling layer built on top of one, because the underlying API still has to provide the same at-most-once guarantee.

This design question is big enough to have its own emerging body of guidance. WSO2's Agent-ready APIs content hub, a set of guides on designing APIs that AI agents can discover and use reliably, lists idempotent tool design as one of its core topics, alongside permission scoping and agent observability. That reflects how central the pattern has become to agent-safe API design.

Idempotency Key Best Practices Checklist​

  • Require an Idempotency-Key (or equivalent header) on POST or PATCH operations whose side effects are worth deduplicating, such as payments, orders, or account creation. This is an API policy decision, not an HTTP rule, and rejecting a missing key on those endpoints with a 400 is reasonable.
  • Generate keys on the client as UUIDs; never derive them from predictable or reused values.
  • Claim the key atomically, before the operation runs, rather than checking for it and writing it in two separate steps.
  • Scope every idempotency record to the caller or tenant and the operation, and authenticate the caller before replaying a stored response.
  • Store a fingerprint of the request body alongside the key, and reject reuse of a key with a different payload.
  • Set an explicit TTL on stored keys instead of keeping them forever.
  • For operations with an external side effect, such as a downstream payment call, don't rely on a local transaction alone. Pass the same key downstream and add a reconciliation path for the crash-in-between case.
  • Document the header name, retention window, and error behavior, so client developers, human or automated, can implement against them correctly.
  • For agent-facing or automation-heavy endpoints, persist the key across retries and resumptions of the same logical write instead of generating a new one for each attempt.

Conclusion​

Idempotency keys solve a narrow problem well: they make retrying a write safe, without the client needing to know whether its previous attempt succeeded. The pattern isn't free. It adds a storage layer and a fingerprint check and, for anything beyond a single local database write, a real design decision about how far the at-most-once guarantee extends and who is allowed to see the stored results.

That cost is worth paying for operations with a genuine side effect, such as payments, orders, and account creation. And it matters more, not less, as more of an API's traffic comes from automated retries and AI agents rather than people clicking through a UI. For a deeper look at what agent-driven traffic means for API design, see WSO2's guides on building agent-ready APIs.

Frequently Asked Questions​

Is idempotency good or bad?​

Idempotency is a design property worth building toward, not a trade-off to weigh. Making a write operation safe to retry removes a whole class of duplicate-execution bugs without meaningfully limiting what the API can do.

Should an idempotency key be required?​

For any POST or PATCH operation with a real side effect, such as payments, orders, or account creation, requiring the key and rejecting requests without one is safer than treating it as optional.

What is idempotency in security?​

It isn't a replay defense in the security sense. An attacker who can submit an authorized request can simply generate a new key and run the operation again. What idempotency limits is accidental duplication: a legitimate retry, a client bug, or a network hiccup gets deduplicated instead of executed twice. Authentication, authorization, and dedicated anti-replay controls still do the actual security work.

What does it mean when an API is idempotent?​

It means repeating the same request produces the same server-side result as sending it once. GET, PUT, and DELETE are idempotent by HTTP specification, while POST and PATCH need an explicit mechanism, such as an idempotency key, to get the same guarantee.

WSO2 API PlatformWSO2 API Platform

The open, universal platform for managing every API and AI service at scale. 100% open source.

Explore

BlogTutorialsTopics
© WSO2 LLC. All rights reserved.
WSO2 LegalDo Not Sell My Personal InformationModern Slavery Statement