Skip to content
Back to skills

Idempotency And Retry Safety

ASecurity

Use when the user asks to "make this endpoint safe to retry", "add an idempotency key", "handle duplicate webhook delivery", "design a payment/booking API", "what happens if this request is sent twice", or is building any endpoint or message consumer that a client, queue, or upstream system might call more than once for the same logical operation. Guides designing idempotency keys and safe retry semantics under at-least-once delivery.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 16, 2026
ai-agentsgosqltestinggitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add shennawardana23/skillme --skill idempotency-and-retry-safety --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Idempotency And Retry Safety?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Idempotency And Retry Safety
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/shennawardana23-idempotency-and-retry-safety/badge)](https://www.skillsdirectory.com/skills/shennawardana23-idempotency-and-retry-safety)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: idempotency-and-retry-safety
description: Use when the user asks to "make this endpoint safe to retry", "add an idempotency key", "handle duplicate webhook delivery", "design a payment/booking API", "what happens if this request is sent twice", or is building any endpoint or message consumer that a client, queue, or upstream system might call more than once for the same logical operation. Guides designing idempotency keys and safe retry semantics under at-least-once delivery.
license: Apache-2.0
metadata:
  version: "0.1.0"
---

# Idempotency and Retry Safety

Distributed systems cannot cheaply guarantee exactly-once delivery — a
client that times out waiting for a response genuinely cannot tell whether
the server never received the request or received it and crashed before
replying. The practical, achievable guarantee is **at-least-once delivery
plus idempotent processing on the receiving end**, which together behave
like exactly-once from the caller's point of view.

## At-least-once vs. exactly-once: why this matters

- **At-most-once**: send once, never retry. Simple, but any dropped
  request or timeout is a silent failure — unacceptable for anything that
  charges a card or books a room.
- **Exactly-once delivery**, as a network-level guarantee, is not
  achievable in general in an asynchronous network with possible partial
  failures (the client cannot distinguish "server never got it" from
  "server got it, processed it, and the ack was lost") — treat any vendor
  claiming true "exactly-once" as actually meaning "at-least-once delivery
  with idempotent processing," which is the real, achievable target.
- **At-least-once + idempotency** is the standard, achievable pattern:
  the client retries freely on any ambiguous failure (timeout, 5xx,
  connection reset), and the server is designed so that processing the
  same logical request N times has the same effect as processing it once.

## Designing an idempotency key

1. **The key identifies one logical operation, generated by the client**,
   not the server — a UUID created once when the user clicks "book" and
   attached to every retry of that same booking attempt. If the server
   generates the key, retries of a request that never reached the server
   can't reuse it, defeating the purpose.
2. **The key's scope must be unambiguous**: usually
   `(idempotency_key, endpoint)` or `(idempotency_key, account_id)` — a key
   that's only unique per-client but collides across unrelated accounts is
   a data-leak risk, not just a bug.
3. **Store the key with the *result*, not just a "seen it" flag.** On a
   duplicate request, replay the original stored response (same status
   code, same body) — don't just skip the operation and return a bare 200,
   which changes behavior from the client's perspective between the first
   and second attempt.
4. **Set a retention window** for stored idempotency keys (commonly
   24 hours) and reject key reuse with a *different* request payload
   within that window as a client error — reusing a key with different
   parameters usually indicates a client bug, not a legitimate retry.
5. **Make the storage write and the business-effect write atomic** — in
   Postgres, insert the idempotency-key row and perform the operation
   inside the same transaction (or make the idempotency key itself a
   unique constraint on the effect table) so a crash between the two
   never leaves the key "claimed" without the effect having happened, or
   vice versa.

## What "idempotent processing" means for different operation types

- **Pure creates** (e.g., `POST /reservations`): naturally non-idempotent
  without a key, since calling it twice creates two reservations — this is
  exactly the case that needs a client-supplied idempotency key.
- **Updates by absolute value** (`SET status = 'confirmed'`): naturally
  idempotent already — applying it twice has the same end state as once.
  No key needed.
- **Updates by relative delta** (`UPDATE inventory SET rooms_available =
  rooms_available - 1`): naturally *not* idempotent — retrying doubles the
  decrement. Either make it idempotent by checking a unique constraint
  first (e.g., a `room_holds` row keyed by reservation ID, checked before
  decrementing) or require an idempotency key exactly like a create.
- **Webhook/event consumers**: the sender almost always guarantees
  at-least-once, never exactly-once — the consumer must dedupe on the
  event's own unique ID (not the payload contents, which can vary between
  redeliveries of the "same" event if the sender includes a timestamp).

## Gotchas

- A `SELECT ... FOR UPDATE` followed by a separate check-then-decrement in
  application code is a race condition under concurrent retries even
  though it "looks" atomic — do the check and the write in one SQL
  statement (`UPDATE ... WHERE rooms_available > 0`) or one transaction
  with proper isolation, not read-then-write in two round trips.
- Idempotency keys stored only in application memory (a local cache) don't
  survive a process restart or work across multiple replicas — they need
  to live in the same durable, shared store (Postgres, Redis) the
  operation itself uses, or a retry that lands on a different replica
  after a restart won't be deduped.
- A payment provider's webhook retry and your own client's HTTP retry are
  two *independent* at-least-once delivery paths — dedup logic must handle
  both, and testing only one (e.g., only simulating client retries) misses
  duplicate webhook delivery as a distinct failure mode.
- Returning a different HTTP status code on retry than the original
  response (e.g., original request returns `201 Created`, replayed
  duplicate returns `200 OK` instead of also `201`) breaks clients that
  branch on status code — always replay the exact original response.
- For our partitioned Postgres tables (partitioned by `hotel_id`), an
  idempotency-key uniqueness constraint must include `hotel_id` in its
  scope and in the query's WHERE clause, or the constraint check itself
  will scan every partition instead of pruning to one.

## Real-world grounding

Stripe's Idempotency Keys feature (publicly documented at
stripe.com/docs/api/idempotent_requests) is the most widely cited
production implementation of this pattern: clients pass a
`Idempotency-Key` header on `POST` requests, Stripe stores the key with
the resulting response for 24 hours, and a retried request with the same
key returns the original response rather than creating a second charge —
directly informing similar idempotency-key APIs adopted since (e.g.,
booking, subscription-billing, and payments platforms as a category).

## Verification

- [ ] Idempotency keys are generated client-side, one per logical operation
- [ ] The stored record includes the original response, not just a "seen"
      flag, so retries replay identical results
- [ ] Key uniqueness constraint is enforced atomically with the operation
      it protects (unique DB constraint, same transaction)
- [ ] Relative-delta updates (increment/decrement) are protected by a
      uniqueness check, not assumed idempotent by default
- [ ] Webhook/event consumers dedupe on the event's own ID, independent of
      client-side retry handling

Files in this skill

  • SKILL.md7.1 KB
  • evals/evals.json3.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.