
An idempotency key is a unique value attached to a request that tells the receiver: "if you've seen this key before, return the same response without doing the work again." It's the single most useful pattern for making distributed systems safe to retry.
This guide explains what idempotency means, how keys work in practice, where they fit in webhook and API design, and the implementation details that separate "we have idempotency" from "we have idempotency that survives production."
What does idempotent mean?
An operation is idempotent if performing it multiple times produces the same result as performing it once.
DELETE /users/42is idempotent — once the user is deleted, repeated deletes don't do anything new.GET /users/42is idempotent — reads don't change state.POST /chargesis not idempotent — each POST creates a new charge.
The HTTP spec tags GET, PUT, and DELETE as idempotent by default. POST is the dangerous one — it usually creates state, and retrying it can create that state twice.
What is an idempotency key?
An idempotency key turns a non-idempotent operation into an idempotent one. The client generates a unique value, the server records it, and any future request with the same value returns the same response without re-running the work.
Concretely, in an HTTP request:
POST /v1/charges HTTP/1.1
Idempotency-Key: 8f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c
Content-Type: application/json
{ "amount": 2999, "currency": "usd", "customer": "cus_O8N3K..." }
The server stores the key with the result. If the same key arrives again — because of a network retry, a queue replay, or a duplicate webhook — the server returns the previous response without charging the customer again.
When do you need idempotency keys?
Anywhere a request might be retried but the operation isn't naturally safe to repeat. The classic cases:
- Payment processing. A timeout on the request could mean the charge succeeded or failed. Without an idempotency key, retrying double-charges the customer.
- Resource creation. Creating a user, a subscription, or a database row. Without idempotency, retries create duplicates.
- Webhook delivery. The sender retries on non-2xx; the receiver might process the same event twice.
- Queue consumers. Most queues guarantee "at-least-once" delivery — duplicates will arrive during failovers.
- Cron jobs that fire on overlap. A scheduler that re-fires a missed run can run the job twice.
If a duplicate of the operation would cause real harm — a double charge, a duplicate user, a doubled email — you need idempotency.
How are idempotency keys used in webhooks?
Two layers, both important.
Inbound (the receiver). Most providers send a unique event ID with every webhook (event.id for Stripe, X-GitHub-Delivery for GitHub, X-Shopify-Webhook-Id for Shopify). The receiver should record the ID and refuse to process the same event twice. Without this, a retry from the provider runs your handler twice.
Outbound (the sender). When you POST a webhook to your customer's endpoint, send a stable message identifier in a header so the customer's handler can dedupe retries. Standard Webhooks uses webhook-id for this purpose.
Hooksbase honors the Idempotency-Key header on HTTP ingest — if a producer sends one, the relay refuses duplicates before they're persisted or counted against quotas. On the dispatch side, every delivery to your endpoint includes a stable webhook-id header (Standard Webhooks-compatible) so your destination can dedupe retries even when the upstream producer didn't send a key.
How to implement idempotency keys
The minimum viable implementation:
- Reserve a key column in a database table (e.g.
idempotency_keys(key TEXT PRIMARY KEY, response BLOB, created_at TIMESTAMP)). - On every incoming request with an
Idempotency-Keyheader, check if the key exists. - If yes, return the stored response.
- If no, process the request, store the response with the key, and return it.
This works. It misses three things production needs.
1. Race conditions
Two retries arrive in parallel before either completes. Both check the table, both see no key, both process. You've lost idempotency.
The fix: insert the key with a status of "pending" before doing the work, in the same transaction. The second arrival sees "pending" and waits (or returns 409 Conflict).
2. TTL and storage growth
Idempotency keys can't live forever — your table will grow without bound. Most systems set a TTL (Stripe uses 24 hours; many systems use 7 days). Pick a TTL longer than the longest legitimate retry window and prune older entries.
3. Scope
Idempotency keys are usually scoped per-endpoint or per-account, not globally. The same key value used by two different customers shouldn't collide. Store the scope alongside the key.
Idempotency keys vs idempotent endpoints
A subtle distinction worth making:
- An idempotent endpoint is one where the operation is naturally safe to retry —
PUT /users/42with the same body always produces the same final state. - An idempotency key lets you make a non-idempotent endpoint safe to retry by deduping at the request level.
Both are valid; they solve different problems. PUT is great for state-setting operations (set the user's name to X). Idempotency keys are right for state-creating operations (create a charge).
How agents use idempotency
For AI agents specifically, idempotency is non-negotiable. Why:
- LLM calls cost money. Re-running a failed event without idempotency burns tokens and can produce different results.
- Tool calls have side effects. An agent that sends an email twice or creates two database rows breaks customer trust immediately.
- Replays must be deterministic. When you ship a fix and replay yesterday's retained failure, you don't want the original side effect to fire again.
The pattern: idempotency at the relay layer (so the agent never sees the duplicate), plus idempotency at the agent's tool-call layer (so even if a tool retries internally, it doesn't double-fire). Both layers, in tandem.
Where to go next
- What is a webhook? for the broader webhook primer
- Recover failed agent events for replay patterns
- Deterministic replay for agents for the replay deep dive
- How to receive Stripe webhooks reliably for idempotency in a provider context