Skip to main content
FomiVIP
Tech Sharing

API Idempotency and the Three Duplicate Orders

One order created three times: the frontend retried on timeout and the backend treated each retry as new. Instead of a distributed lock, we added a client-generated request_id as a unique index — duplicates get the first result back. Raising the timeout from 3s to 8s also cut retries visibly.

September 16, 2022IdempotencyAPI DesignLessons Learned

On the morning of September 16, customer service forwarded us three nearly identical orders: same user, same product, less than two seconds apart. Same amount, same shipping address — only the order numbers differed.

Why It Happened Three Times

The frontend had a 3-second timeout; on a slow network it would auto-retry before the response arrived, up to two retries. The backend order endpoint had no deduplication at all — one request in, one row inserted. All three attempts succeeded.

This was different from the earlier loyalty-points incident: that time the user was clicking manually; this time the program was clicking on its own, and the user had no idea.

Where to Put the Idempotency Key

We discussed combining user id + product id + timestamp, but change the timestamp and it is no longer a duplicate — that protects nothing. In the end we used a client-generated request_id: a uuid is created as soon as the checkout page loads and sent with the submission, and retries reuse the same one.

The server puts a unique index on request_id in the orders table. A duplicate insert fails; we catch the conflict and — instead of returning an error — look up the existing order and return it as-is. The frontend gets the same order number, so it never assumes failure and tries again.

The key point remains: duplicate requests should return the original result, not an error.

Two Things We Fixed Along the Way

  • The frontend timeout went from 3 seconds to 8. Most 'timeouts' are really just slowness, not a missing response. After the change, retries dropped visibly.
  • Retries apply only to clear network errors; no retry on 5xx or business errors. The first version didn't distinguish — a business message like 'out of stock' also triggered two retries. Wasted effort.

Cleaning Up the Data

Of the three orders already created, we kept one and ran the other two through the cancellation flow — no hard delete, with a note in the cancellation record: 'merged as duplicate submissions.' Payment had only captured once, so no double charge. Lucky, all things considered.

We exported a detail sheet for customer service — seven columns — so they could walk the user through it.

Afterward

We went through every creation-type endpoint, nine in total, checking each for an idempotency key. Four had never caused problems; we added keys to them anyway. The review checklist gained one line: what happens if this endpoint gets called twice?

The screenshots of those three orders were not deleted. They live in the 'Duplicates' section of our internal docs, right next to 'Two Hours.'

Consulting · Free initial chat

Need advice for your scenario?

Tell us your business context and current systems, and we'll give an actionable approach.

Contact us

Mon–Fri 9:00–18:00 · We respond on business days within one business day.

WeChat (Business Only)
WeChat (Business Only)二维码

Scan and add our WeChat for consulting

Or fill out the requirements form