What they're testing
Whether you think about the lost RESPONSE case, which is a different problem from the lost request.
The short answer~30 seconds
The client generates a unique key per intent (usually a UUID) and sends it in an Idempotency-Key header. The server stores that key with the result in a table with a UNIQUE constraint; if the key already exists, it returns the stored response rather than performing the work again. The crux is that recording the key and doing the work must be in the SAME transaction, otherwise you've narrowed the race window rather than closing it.
The long answer
The hard part is who generates the key. If the server does, it's useless — each retry is a new request with a new key. The key must come from the CLIENT, tied to the user's intent (one button press), and stay identical across every retry. This usually has to be stated explicitly in the API docs, because clients don't invent it on their own.
The in-flight state needs handling too: two requests with the same key arriving AT ONCE. The usual approach inserts the key row as in_progress up front; if the insert conflicts, either wait briefly and read the result, or return 409 for the client to retry. Skipping this case is why systems that "have idempotency" still double-book under load.
Finally, expiry. Keeping keys forever grows the table without bound; expiring too early lets a late retry create a duplicate. Twenty-four hours is the common choice (Stripe uses 24 hours), and it's worth storing a hash of the body too, so a client reusing a key for different content gets a 422 rather than silently receiving the old result.
CREATE TABLE idempotency (
key text PRIMARY KEY,
request_hash text NOT NULL,
status text NOT NULL, -- in_progress | done
response jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
-- Cùng MỘT transaction với việc tạo đơn:
BEGIN;
INSERT INTO idempotency (key, request_hash, status)
VALUES ($1, $2, 'in_progress'); -- trùng khoá -> lỗi, xử lý ở tầng trên
INSERT INTO orders (…) VALUES (…);
UPDATE idempotency SET status = 'done', response = $3 WHERE key = $1;
COMMIT;What they'll ask next
?Why not just use PUT to create?
You can, when the client knows the identifier up front: PUT /orders/{client-generated-uuid} is idempotent by the spec, no header needed. It only fails when the id is server-generated, or when the operation has external side effects such as a charge.
?What if the third party doesn't support idempotency keys?
Then you reconcile: record the intent before calling, and afterwards query the other side by your own reference to see whether it happened before retrying. Slower and more involved, but it's the only option when you don't control the far end.
These lose points
- "Check whether the order exists, then create it." That's an unlocked read-modify-write — precisely the race you're trying to fix.
- Storing the key in Redis without the result. The retry gets a 409 instead of the created order, leaving the client with no way to learn its id.