DevOps

How to Make API Retries Safe With Idempotency Keys

A timeout does not tell you whether the request worked. Idempotency keys let clients retry without charging a customer twice.

Paige Donnelly

Senior Backend Engineer

Published 8 min read
Close-up of a person entering a credit card for online shopping on a laptop.
On this page 6 sections

An idempotency key is a unique value a client sends with a request so the server can recognize a retry and return the original result instead of repeating the action. To use them, generate a key per operation, store it with the response, and reject reuse with different parameters.

This guide is for backend engineers who run APIs that move money, create orders, or send messages. Backtrace writes about the tradeoffs of production systems, and duplicate side effects are one of the most expensive bugs we see described in postmortems. The advice here is evergreen and applies to any HTTP API.

Why API Retries Create Duplicate Charges

A client sends a payment request and the connection drops. The client sees a timeout and knows nothing else. The server may have processed the charge, may have failed halfway, or may never have received the call.

The only safe-looking choice is to retry, and retrying is exactly how duplicates happen. A load balancer can also retry on its own, and so can a mobile app that comes back online. Each layer is trying to be helpful, and together they can fire the same request three times.

The HTTP standard already draws a line here. The IETF's RFC 9110 section on idempotent methods says PUT and DELETE are idempotent by definition, meaning repeating them has the same effect as sending them once. POST has no such guarantee, and POST is the method most create and charge endpoints use.

Consider a hypothetical API that handles one million payment requests a day. If just 0.1 percent of them time out and get retried, that is 1,000 potential duplicate charges every day. Each one becomes a support ticket, a refund, and a little less trust.

A blue network patch cable with an RJ45 connector.

Idempotency keys solve this by making the retry safe instead of trying to prevent it. The server remembers what it did the first time. A second request with the same key gets the same answer and causes no new side effect.

What Makes an Idempotency Key Work?

A good key identifies one logical operation, not one HTTP attempt. The client creates it before the first try and reuses it on every retry of that operation. A new operation, such as a second purchase, gets a new key.

Use a random UUID version 4 or a similarly unguessable string. Do not build keys from user ID and timestamp, because two legitimate requests can collide, and a predictable key invites abuse. Most providers accept up to a few hundred characters, so length is rarely the limit.

The server needs to do more than look the key up. It should also store a fingerprint of the request, such as a hash of the body and the target path. If the same key arrives with different parameters, the server must reject it with an error. Otherwise a client bug could silently return the wrong saved result.

Stripe's documentation on idempotent requests is a useful real-world model. It saves the status code and body of the first request, including errors, and returns them for later requests with the same key. It also says keys can be removed from the system after they are at least 24 hours old.

Close-up of colorful programming code displayed on a computer screen.

Scope each key to the caller. A key should be unique per account or API credential, not global. Otherwise one customer's key can collide with another's, and a malicious client could probe for keys that belong to someone else. Credentials need the same care, a topic we covered in our guide to API key rotation.

Where Should You Store Idempotency Keys?

Store them in the same database transaction as the work they protect, in a table with a unique constraint on the key. That way the key and the side effect commit together or not at all. A cache alone can lose entries and let a retry run twice.

The unique constraint is what makes this safe under concurrency. Two retries can arrive at the same moment on different servers. Both try to insert the key, one wins, and the other receives a conflict error that it can turn into a "request in progress" response.

A minimal table needs a handful of columns. The list below names them and what each one holds.

  • account_id and key, together forming the unique constraint.
  • request_hash, the fingerprint used to detect a mismatched retry.
  • status, which is started, completed, or failed.
  • response_code and response_body, saved from the first attempt.
  • created_at, used to expire old rows.

Adding this table to a busy database is a schema change like any other. Create it before the code that uses it ships, and use the approach described in our walkthrough of zero-downtime schema migrations. A new table is low risk, but a new index on a large existing table is not.

Server hardware representing cloud storage for saved request records.

If the side effect lives outside your database, such as a call to a card processor, the transaction cannot span both. In that case, pass your own key downstream to the processor as well. Then a retry of your request becomes a retry of theirs, and they dedupe it for you.

How to Add Idempotency Keys in Five Steps

Roll the feature out on one endpoint first, ideally the one with the costliest duplicate. Follow these steps in order.

  1. Pick the endpoint, and require or accept an Idempotency-Key header on its POST route.
  2. Create the key table with a unique constraint on account and key.
  3. On each request, insert the key with status started, and return a conflict if the insert fails.
  4. Run the operation, then save the response and mark the key completed in the same transaction.
  5. On a repeat request, compare the request hash, then replay the saved response with a header that marks it as a replay.

Decide whether the header is required or optional. Required keys are safer for payment endpoints because they prevent a careless client from skipping protection. Optional keys are easier to adopt for existing clients, and you can tighten the rule later.

The table below summarizes how the server should respond in each situation. Treat it as the contract your documentation promises.

How to Add Idempotency Keys in Five Steps
SituationServer actionTypical response
New keyProcess and save the resultNormal success or error
Same key, same request, finishedReplay the saved resultOriginal status and body
Same key, same request, still runningDo nothing new409 Conflict, retry later
Same key, different requestReject422 or 400 with a clear message
Key past expiryTreat as newNormal processing

Teams often ask whether to save failed results. For client errors such as validation failures, saving is fine, since the same input will fail again. For server errors, think harder. Some providers replay even a 500, while others release the key so the client can try again after a fix.

Roll the change out behind a flag so you can turn it off quickly. The practices in our feature flags guide apply directly. Enable it for internal traffic, then a small share of clients, and watch the conflict rate before widening.

Edge Cases That Trip Teams Up

The first edge case is a crash between the side effect and the saved response. If you write both in one transaction, this cannot happen. If you cannot, the operation needs its own recovery job that checks started keys older than a few minutes and finishes or cancels them.

The second is key lifetime. Expire keys on a published schedule, such as 24 hours or seven days, and say so in your docs. A client that retries after the window will create a duplicate, and that is a documented limit, not a bug.

The third is clock and ordering trouble in distributed setups. Do not rely on timestamps to decide which request came first. Let the database constraint decide, since it is the single point that sees every attempt.

One honest limit remains. Idempotency keys protect against duplicate delivery, not against a client that sends two different keys for the same intent. Pair them with sensible client behavior, such as disabling a submit button after one click.

Test the contract before clients depend on it. Write automated tests that send the same request twice, send it twice at once, and send it with a changed body. Each test should confirm that only one side effect happens and that the error messages are clear enough for a client developer to act on.

Conclusion

Safe retries come from a simple contract. The client names the operation, the server remembers the outcome, and a repeat returns the same answer. A unique constraint and one transaction do most of the work.

Start with your most expensive endpoint this sprint. Add the key table, ship the replay path behind a flag, and watch the conflict rate for a week. Backtrace readers who run payment or order APIs should treat this as baseline hygiene rather than an advanced feature.

Do GET requests need idempotency keys?
No. GET requests are defined as safe and idempotent, so repeating them does not change server state. Idempotency keys matter for POST and PATCH requests that create or change data. PUT and DELETE are already idempotent by definition, though keys can still help when their handlers trigger side effects.
How long should I keep idempotency keys?
Keep them longer than your longest realistic retry window. Many providers use 24 hours, and some use several days. Publish the number in your docs, delete expired rows with a scheduled job, and remember that a retry after expiry will be treated as a brand new request.
What status code should a duplicate in-flight request return?
Return 409 Conflict with a message saying the original request is still processing, and include a Retry-After header if you can. The client should wait and retry with the same key. Once the first request finishes, the retry receives the saved response.

Written by

Paige Donnelly

Senior Backend Engineer

8+ years experience

Paige Donnelly is a senior backend engineer who has spent the last eight years working on distributed systems, first at a mid-size fintech and then leading a payments infrastructure team through a migration from a monolith to event-driven services. She's on the losing end of more 3am pages than she'd like to admit, which is why she writes about observability, deployment safety, and API design with an eye toward what actually breaks in production rather than what looks good in a conference talk. She contributes to a couple of open-source CLI tools in her spare time and still refuses to use a debugger for anything she can solve with well-placed logs.

Credentials

  • 8 years in backend and distributed systems engineering
  • Former tech lead, payments infrastructure at a Series D fintech
  • Contributor to several open-source developer tooling projects