How to Add Idempotency Keys to an AI-Built App
Retries, timeouts, and double-clicks should never double-charge a customer. Here is how idempotency keys work, the exact table schema, and the prompt that avoids the in-memory-cache trap.
How to Add Idempotency Keys to an AI-Built App
Idempotency keys make an AI-built app safe to retry: a client sends a unique key with a write request, the server checks whether it has already processed that exact key, and if so returns the original result instead of doing the work twice. Without this, a network timeout followed by an automatic retry, or a user double-clicking a slow button, can charge a card twice, send a duplicate email, or create two of the same record, and an AI coding agent asked for "a checkout endpoint" will not add this protection unless you ask for it by name.
Why retries happen even when everything is working correctly
A request succeeds on the server but the response never reaches the client, a dropped connection, a timeout, a mobile network switch, so the client has no way to know the write already happened and retries.
A user double-clicks a submit button before the UI has disabled it, sending two identical requests seconds apart.
A background job framework retries a failed job automatically, and the job's own write already partially succeeded before the failure that triggered the retry.
None of these require a bug in your code. They are normal network and UI behavior, which is exactly why the fix belongs at the API layer, not in "be more careful with retries" advice to whoever calls it.
The table and the request flow
create table idempotency_keys (
key text primary key, -- client-generated, e.g. a UUID
request_hash text not null, -- hash of the request body, catches key reuse with different data
status text not null default 'processing', -- processing | completed | failed
response_body jsonb,
response_status int,
created_at timestamptz default now()
);
-- On each request carrying an Idempotency-Key header:
-- 1. Try to insert a new row for that key (status='processing').
-- If the insert conflicts, the key was already used:
-- - status='completed' -> return the stored response, do not redo the work
-- - status='processing' -> another request with this key is in flight, return 409
-- - status='failed' -> safe to retry the underlying operation
-- 2. If the insert succeeded, do the real work, then update the row
-- with status='completed' and the response to store.The request_hash column matters more than it looks: it catches the case where a client reuses an idempotency key with a different request body, a bug on the client side, and lets you return a clear conflict error instead of silently serving a mismatched cached response.
The prompt that gets this right the first time
Add idempotency key support to [this checkout/payment endpoint]. Accept
an Idempotency-Key header from the client. Store keys in a database
table (not in memory), keyed on the idempotency key, with a hash of
the request body, a status (processing/completed/failed), and the
stored response. On a repeated key with status completed, return the
original stored response without re-running the operation. On a
repeated key still processing, return 409 Conflict. On a repeated key
with a different request body hash, return 422 with a clear error.
Expire and clean up rows older than 24 hours.Naming the table-backed requirement explicitly matters here more than in most features: asked generically for "idempotency," an agent will sometimes suggest an in-memory Set or cache of seen keys, which loses all protection on a restart or across multiple server instances, the exact failure mode that made the feature necessary in the first place.
Where this belongs, and where it does not
Any endpoint that charges money, sends a payment, or moves a balance needs this without exception.
Any endpoint that sends an email, SMS, or push notification benefits from it, a duplicate notification is a minor annoyance rather than a financial error, but still worth preventing.
A plain GET request never needs an idempotency key, reads are naturally safe to repeat. The feature is specifically for writes with real-world side effects.
Where this connects to features you may already have
If you have already wired up payments, idempotency keys are the single highest-leverage addition you can make to that flow, most payment providers (Stripe included) support and expect an idempotency key on their own API calls, and propagating the same discipline to your own endpoint closes the gap between their protection and yours. If you already log administrative and financial actions in an audit log, a rejected duplicate request is worth a log entry too, it is a useful signal for debugging client-side retry behavior later.
This is part of the same reliability layer as rate limiting: one protects your infrastructure from too many requests, the other protects your data from the same request landing twice. Most production apps need both.
Frequently asked questions
Do I need to build this myself, or does my payment provider handle it?
Your payment provider's idempotency protects their side of the call: it stops you from double-charging through their API. It does nothing for your own endpoint, if a client retries the request to your server before your server even calls the payment provider, you can still process the same order twice on your side without your own idempotency layer.
How long should I keep idempotency key records?
Long enough to cover realistic retry windows, 24 hours is a common, safe default for most apps. Keeping them indefinitely wastes storage for no benefit, a client retrying a request from a week ago is not the scenario this feature protects against.
What should the client generate the key from?
A fresh random UUID generated once per logical user action (once per checkout attempt, not once per HTTP request), stored client-side, and reused automatically if that same action is retried. If the user starts a genuinely new action, like clicking checkout again after abandoning the first attempt, generate a new key.
Does this replace the need for database transactions?
No, they solve different problems. A transaction keeps a single operation atomic while it runs. An idempotency key prevents the same operation from being started twice in the first place. You typically want both: an idempotency check wrapping a transactional write.
How did this land?
About the author

Developer Advocate
Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.


