Sep 21, 2025·5 min read

API idempotency: stop duplicate creates with retry-safe design

API idempotency helps prevent duplicate creates and double charges by using request IDs, unique constraints, and safe retry rules for your endpoints.

API idempotency: stop duplicate creates with retry-safe design

Why duplicate creates happen in real life

Duplicate creates usually aren't caused by "bad users". They happen because real networks and real people are messy.

A phone on shaky Wi-Fi sends a request, the app shows a spinner, and the user taps the button again. Or the request reaches your server, but the response never makes it back, so the client retries.

Retries also happen automatically. Mobile apps, browser fetch libraries, API gateways, and background job runners often retry after a timeout, a dropped connection, or a 502/503 error. From the client’s point of view, retrying is the safe move: "I’m not sure it worked, so I’ll try again."

In practice, a "duplicate create" looks like this:

  • Two orders created for one checkout
  • Two support tickets for one complaint
  • Two subscriptions or charges for one click
  • Two invites sent to the same person

The scary part is that the server often can’t tell the difference between a brand-new request and the same request repeated. If your POST endpoint always inserts a new row, every retry becomes a second row. The client might not even show it, but your database and your customers will.

AI-generated backends are especially likely to miss this because they tend to focus on the happy path: one click, one request, one response. They often don’t model timeouts, double taps, or racing requests.

The goal is simple: if the same create request is repeated, it should produce the same result, not a second action. That’s what API idempotency protects.

API idempotency in plain English

API idempotency means: if the same request is sent more than once, the result is the same as if it were sent once.

This matters because many systems effectively operate with "at least once" delivery. A request might arrive one time or two times, but it will usually arrive.

Some actions are naturally idempotent. A typical GET request can be repeated without changing anything. Refreshing a page shouldn’t create a new user.

Other actions are not naturally idempotent. A typical POST that creates something (an order, a user, a payment charge) can run twice and produce two records or two charges. If the first POST succeeded but the response was lost, the client retries and accidentally creates a duplicate.

Idempotency gives you a safe way to retry. It doesn’t prevent every failure, and it won’t fix broken business logic, but it does prevent double actions when retries happen.

Where idempotency matters most

You don’t need idempotency everywhere. You need it where a retry can trigger a second real-world action: more money captured, more emails sent, more jobs started, more rows created.

Prioritize endpoints that create or trigger something that’s hard to undo. Common examples:

  • Payments (charge, authorize, capture)
  • Orders, subscriptions, bookings
  • Email/SMS sends (password reset, invites, receipts)
  • Background jobs (exports, processing, report generation)
  • Webhook handlers that create or grant something

Also watch for endpoints that look like "updates" but behave like additions. Anything that increments counters, appends items, applies credits, or changes totals can double-apply on retries.

Idempotency keys (request IDs): the basic pattern

Idempotency keys are the most practical way to make write requests safe to retry.

The idea is simple: the client sends a unique request ID with a write request (usually a POST), and the server promises that repeating the same request ID won’t create a second resource or repeat the action.

A common approach is to accept a header like Idempotency-Key (or a requestId field in the body). If the client times out and retries, it reuses the same key.

On the server, you store a small record per key. Most teams keep:

  • the key
  • who it belongs to (scope)
  • which operation it applies to (endpoint, method)
  • status (in_progress, succeeded, failed)
  • the response that was returned (status code and body)

Scope matters

A key shouldn’t be global across your whole system. Good defaults scope it to one of these: per user, per account/tenant, or per API token, and usually per endpoint.

For example, a key used for POST /orders shouldn’t be accepted for POST /refunds, even if the string matches.

Retention is a tradeoff

Keep keys long enough to cover real retries (minutes to hours), plus buffer for slow clients and queues. Some teams keep them for 24 hours or a few days to protect against accidental replays. Longer retention reduces duplicates, but increases storage and forces you to plan cleanup.

Replay behavior

When the same key is replayed, the server should return the original result, not run the action again.

Example: a checkout returns 201 with order_id=123. If the client retries with the same key, return the same 201 and the same order_id.

Unique constraints: the safety net that catches races

Repair an AI Built Backend
If your AI-generated backend works in demos but breaks in production, we’ll diagnose and repair it.

Idempotency keys help, but they’re not enough on their own. Your last line of defense is the database.

When two requests hit your API at almost the same time, only the database can reliably stop both from creating the same thing.

A unique constraint tells the database: "there can only be one row like this." Even if your code runs two copies of the request in parallel, the database rejects the duplicate.

Common examples:

  • Unique email in a users table
  • Unique pair like (account_id, external_id) when importing from Stripe, QuickBooks, or a CRM
  • Unique order_number

A practical pattern is to store the request ID on the record you create. For example, add an idempotency_key column on orders and make it unique, often scoped like (account_id, idempotency_key). Then every retry maps to the same row.

When the constraint is hit, your API usually does one of two things:

  • Return the existing record (best for "create order", "start checkout", "create invoice")
  • Return a clear conflict error (better when reusing a key could hide a real mistake)

Don’t rely on "check then insert" in application code. Two workers can both check "does it exist?" and both see "no" before either inserts. Make uniqueness a database rule.

How to add idempotency to a POST endpoint (step by step)

If a client times out and retries a POST, you want the second call to return the same result, not create a second record.

Start with two decisions:

  • Which actions cause real damage when repeated (orders, charges, invites, jobs)
  • What the key is scoped to (per endpoint + user/account is a solid default)

Then implement it in a way that stays correct under concurrency.

1) Require a stable request ID

For risky endpoints, require an Idempotency-Key header (or a request ID field). Clients must reuse the same value for retries.

2) Persist the key

Either create an idempotency table (key, endpoint, user/account, status, response), or store the key directly on the created record when there’s a clean 1:1 mapping.

3) Claim the key atomically

Insert the key record first, protected by a unique constraint (or take a lock). This is how you prevent two concurrent requests from both "winning."

4) Store the response you return

After the action succeeds, store the response (status code and body). On repeats, return that stored response without re-running the work.

5) Decide what to do when a request is in progress

If a retry arrives while the first attempt is still running, you need predictable behavior. Common options are:

  • wait briefly, then re-check
  • return 409 Conflict (or 202 Accepted) with a clear "still processing" message

Pick one approach and keep it consistent.

Handling tricky cases: timeouts, concurrency, and partial failures

Retries get messy when the client and server disagree about what happened. Idempotency makes those moments boring: same request, same outcome.

Timeout after success

The server created the record, but the response never arrived. Without protection, the retry creates a second record.

Treat the idempotency key as the receipt. If the retry uses the same key, return the original result.

Crash mid-flight and concurrent retries

Storing a key isn’t enough if you don’t track what happened.

A practical set of rules:

  • Store status: pending, succeeded, failed.
  • Ensure only one request can own the key (unique constraint is the simplest guard).
  • If a retry finds pending, don’t start a second run. Wait briefly or return a clear "still processing" response.
  • After success, always return the same response for the same key while it’s retained.

Partial failures

This is the hardest case. You might create a user but fail to send the welcome email, or capture payment but fail to create the order row.

Pick a clear "source of truth" for the request, and make sure retries don’t repeat side effects that already succeeded. Often that means:

  • finishing the remaining steps asynchronously
  • using a compensation step (for example, refunding a payment when the order can’t be created)

Common mistakes that still allow duplicates

Fix One Endpoint Completely
If duplicates are already happening, we’ll fix one critical endpoint end-to-end first.

Most duplicate creates aren’t caused by missing idempotency entirely. They happen because idempotency is added halfway.

Common failure modes:

  • Making the idempotency key optional, so only some requests are protected.
  • Saving the key but not saving the result, so a retry still re-runs the work.
  • Not scoping keys (a key reused across users or endpoints can collide).
  • Doing "check then insert" without a database unique constraint.
  • Treating side effects like "send email" as idempotent without tracking whether it was sent.

A classic scenario is POST /orders that charges the card first, then crashes before returning a response. If the retry charges again, you get a double charge. Avoid that by persisting the outcome and guarding it with uniqueness.

Quick checklist to verify retry-safe behavior

A retry-safe API should behave the same way every time the same action is repeated.

For write endpoints (POST, and sometimes PATCH/DELETE):

  • Require an Idempotency-Key for operations that create or trigger something.
  • Enforce a database unique constraint for the dedupe rule.
  • Verify retries return the same resource ID and the same response body (or the same stable representation).
  • Define key scope (per user/account + per endpoint is a good baseline).
  • Keep keys long enough to cover real retries, and log enough context to debug.

For webhook handlers:

  • Treat the provider’s event ID as the idempotency key, store it, and return success on repeats.

A simple test is to send the exact same request five times quickly (including the same key). You should see one create and four "same result" responses.

Example: preventing double charges in a flaky checkout

Stop Duplicate Creates Fast
We’ll find the endpoints where retries can double-charge, double-create, or spam users.

A solo founder is testing a checkout that started as an AI-generated prototype. It works in demos, but in real use the network is shaky and the UI sometimes hangs on a spinner.

A customer taps "Pay" once. Nothing seems to happen. They tap again. Both requests reach the API.

Without idempotency, the backend treats these as two separate purchases. You can end up with two successful payments, two order rows, and a support ticket that starts with: "I only clicked once." The customer may even file a dispute because they can’t tell which charge is valid.

With an idempotency key plus a database unique constraint, the second request doesn’t create anything new. The server recognizes it as a retry and returns the same result as the first call: the original order ID and payment status.

What this usually looks like:

  • The client generates one idempotency key when the user presses "Pay"
  • POST /checkout stores that key with the order attempt
  • The database enforces uniqueness on (user_id, idempotency_key) or (merchant_id, idempotency_key)
  • On retry, the API fetches the existing record and returns it

Support gets easier if you log a few fields: idempotency key, resulting order ID, payment provider charge ID, timestamp, and whether a request was a replay.

Next steps for AI-generated APIs that break under retries

AI-generated backends often look fine in a demo, then break when users refresh, mobile networks drop, or providers retry webhooks. Stabilize the endpoints that can do real damage when they run twice.

Pick your top three risky operations (often payments, orders, invites, and inbound webhooks). Add two layers:

  • Idempotency keys to make retries safe on purpose
  • Database unique constraints to catch races

Then run a small, realistic test: double-click the submit button, simulate a client timeout and retry with the same request ID, and fire two concurrent requests with the same payload. You want one real create and consistent "same result" responses.

If you inherited an AI-generated codebase and duplicates are already happening, it’s often faster to do focused remediation than a full rewrite: fix one endpoint end-to-end, then move to the next. If you want a second set of eyes, FixMyMess (fixmymess.ai) specializes in diagnosing and repairing AI-generated backends, including adding idempotency, uniqueness safeguards, and production-ready retry behavior.

FAQ

Why am I seeing duplicate records even when users swear they clicked once?

Because networks and apps retry. A request can succeed on the server but the response gets lost, or a user double-taps while the UI is stuck. If your POST always inserts a new row, every retry can become a second create.

What does “API idempotency” mean in plain terms?

Idempotency means repeating the same request produces the same outcome as sending it once. For create actions, that usually means the same resource is returned on retry instead of creating a second one.

Which endpoints should I make idempotent first?

Use it on actions where a retry can trigger a real side effect you don’t want twice, like charging money, creating orders, sending invites, or starting long jobs. You usually don’t need it for reads, and you may not need it for harmless updates.

What is an idempotency key and how is it used?

An idempotency key is a client-generated request ID sent with a write request, often via an Idempotency-Key header. The server records that key and, if it sees the same key again, returns the original result instead of re-running the action.

How should I scope idempotency keys so they don’t collide?

Scope it to the actor and the operation. A good default is per account or user plus the endpoint and method, so the same string can’t accidentally apply to a different user or a different action like refunds.

How long should I store idempotency keys?

Keep them long enough to cover real retries and delayed replays, typically hours to a day for many products. Longer retention reduces duplicate risk but increases storage and requires cleanup, so pick a window you can enforce consistently.

What should the server return when it receives the same idempotency key again?

Return the original response for that key, including the same status code and body, and do not repeat the side effect. This makes client retries safe and predictable, especially after timeouts or dropped connections.

Why do I still need a database unique constraint if I have idempotency keys?

Treat the database unique constraint as the final guard against races. Two requests can pass “check then insert” at the same time, but the database can enforce “only one row like this,” letting you fetch and return the existing record on conflict.

What happens if a retry arrives while the first request is still running?

You need a clear rule for “pending” keys so you don’t run the action twice. Common approaches are to wait briefly and re-check, or return a clear “still processing” response, then ensure the final stored outcome is what retries will get.

How can I quickly test that my API is safe to retry?

Start by sending the exact same request multiple times with the same idempotency key and confirm you get one create and consistent repeat responses. If you inherited an AI-generated backend that breaks under retries, a focused remediation is often fastest; FixMyMess can audit the risky endpoints and add idempotency plus database constraints end-to-end.