Install
openclaw skills install skills-sh:samber/developer-platform-skills/api-idempotency-retryAPI Idempotency and Retry You design the two halves of one guarantee: server-side idempotency keys that make a retried write safe, and client-side retry behavior that uses them responsibly. Shipping either half alone is the trap - Resilience4j's own docs state it as a hard…
openclaw skills install skills-sh:samber/developer-platform-skills/api-idempotency-retryYou design the two halves of one guarantee: server-side idempotency keys that make a retried write safe, and client-side retry behavior that uses them responsibly. Shipping either half alone is the trap - Resilience4j's own docs state it as a hard constraint, not a suggestion: retried operations must be idempotent, so retry guidance without key support (or the reverse) leaves the guarantee open exactly where a dropped connection tests it.
Two facts frame everything below:
Idempotency-Key header. The IETF draft (draft-ietf-httpapi-idempotency-key-header) expired in April 2026 without ratification. The header name is consistent across the industry because Stripe popularized it, but the semantics (TTL, mismatch behavior, concurrency handling) are whatever each provider decided, so yours must be designed and documented, never assumed.Memory (advised): When memory lives in a file, consider using developer-platform-context.md; if a different memory system is in use, rely on that instead. The file is an advisory reference, not a mandatory requirement. Separate task info in different sections. Remove finished tasks. Add a date to a task; no date for general project context. Some interview responses may differ between 2 tasks.
Ask these before designing anything; each answer changes a later step. Batch them - this is a tactical design task, not a strategy interview.
Every retry decision is made by a developer or a program, whoever bought the product. The design splits by who controls the retry loop:
Design for raw-HTTP integrators first - a contract complete enough for them is what your own SDKs then automate - and treat SDK defaults as the highest-leverage artifact, since they decide behavior for the majority who never read the docs.
Each step has a section below, in order.
Idempotency-Key retrofit pattern.samber/developer-platform-skills@public-api-design-review performs; this skill builds what makes the first option true.(account_id, idempotency_key) - never globally. Stripe, AWS, and the standard Postgres reference implementation all key on the pair, so the same key string reused by different accounts never collides.event-derived > request-scoped > random UUID reused across retriesevent-derived > request-scoped > random UUID reused across retriesevent-derived > request-scoped > random UUID reused across retries - value and effort rank identically here, so their spread decides the ratio: minutes wide on effort (name the key after an identifier the caller already holds) against the whole guarantee on value.order-confirm-${orderId}) - deterministic by construction, so a retry cannot fail to reuse the key. Move down to request-scoped when no stable business identifier exists at call time, and to a reused random UUID when the caller is a stateless script. Nothing is starved here: every rung costs one line of client code.Date.now(), randomUUID() regenerated on retry) defeats the mechanism entirely, so document it as the anti-pattern instead.409 invalid_idempotent_request), never silently replay. The expired IETF draft generalizes this into an optional payload fingerprint stored beside the key; a checksum of the payload is the simplest form.A check-then-insert is a TOCTOU race under concurrent retries: both callers see "absent" and both proceed. The claim must be a single atomic operation the storage layer arbitrates. Three mechanisms appear in production, ranked:
efficiency: DB unique constraint > conditional-write OCC > row lock + SERIALIZABLE
effort: row lock + SERIALIZABLE > conditional-write OCC > DB unique constraint
value: row lock + SERIALIZABLE > DB unique constraint == conditional-write OCC
Default rung: a unique constraint on (account_id, idempotency_key). The database picks the single winner; the loser gets its 409 from the INSERT failure itself, not an application-level check. Nearly free - one index - and transaction isolation gives the fencing guarantee below at no extra cost.
Conditional-write OCC (DynamoDB-style) delivers the same single-winner claim on stores without unique-constraint transactions - the tie on the value line is real: same guarantee, different storage family. Your answer to question 3 rules one of these two out rather than ranking it; a ruled-out option is deleted, not demoted.
Row lock + SERIALIZABLE transaction is the starved option.
locked_at column inside a SERIALIZABLE transaction represents in-flight as a first-class state, survives lock expiry, and anchors resumable recovery points.Whatever the mechanism: the claim and the business mutation must commit as one ACID unit - AWS states this as a hard requirement, not a best practice. A claim outside the transaction it guards can record a token for a resource that was never created, or the reverse.
A bare Redis SETNX + TTL lock is not a fourth option. Kleppmann's analysis: without a fencing token, a client paused past its lease expiry can still write after the lock moved on - no timing assumption saves you. No surveyed vendor uses a distributed lock for the claim; if Redis is all you have, layer fencing tokens on top, and know the DB constraint sidesteps the problem entirely.
This ranking is a default, not a law. Re-rank against the storage answer and the effort ceiling (questions 3 and 7): a team already running SERIALIZABLE workloads gets the starved option far cheaper than the effort line assumes. A ship date that forbids a schema migration promotes whichever mechanism their current store already supports.
The bug in most homegrown implementations is treating a key as binary - seen or not-seen. A key is a three-state machine: absent, in-flight, complete. The retry that hits an in-flight key is not the bug ("your client library did exactly what it was configured to do when the connection dropped"); the bug is a server with no way to say "seen, not finished."
| Outcome | What happened | Correct behavior |
|---|---|---|
| Never received | Network drop before processing | Retry proceeds as a first attempt; the claim wins normally |
| Received, not processed | Claim held, no terminal result yet | Return an in-flight conflict, or resume from the last recovery point once the original lock expired |
| Processed, response lost | Result committed; response never arrived | Replay the stored response verbatim - no re-execution |
For the in-flight case, pick one:
409 concurrent_idempotent_requests, documented advice "wait and retry") - simplest and safest default.Never let the second caller through because the first "seems stuck." The expired IETF draft deliberately made in-flight conflict and completed replay different responses, not fallbacks of each other.
For multi-step requests (question 4), decompose into atomic phases separated by each foreign state mutation, persisting a recovery point between phases - a retried request resumes exactly where it left off instead of re-running an external call already made. Record intent before calling out, so a crash between call and response leaves evidence.
Run recovery as active background jobs, not passive expiry (Brandur's model):
What the error responses look like - codes, envelope, retryable flag, Retry-After - is samber/developer-platform-skills@api-error-design's territory; consume its signals, don't redesign them here. Distinguishing your two 409 meanings (payload mismatch vs in-flight) with distinct machine-readable codes is that skill's taxonomy applied to this skill's states.
See references/key-lifecycle-walkthrough.md for the full worked lifecycle: schema, claim query, all three states' responses, and a multi-step recovery-point example.
Retry-After), 502, 503, 504, connection timeouts and resets - 500 cautiously (some 500s are a real bug that retries identically). Never retry non-429 4xx, auth failures, or exceptions that are bugs. Four independent sources (tenacity, Resilience4j, Resend, and the response-signaling side) converge on this exact split - cite it as convergent practice, not one vendor's opinion.full jitter > decorrelated jitter > equal jitter > no jitterfull jitter == decorrelated jitter == equal jitter == no jitter - a real tie, not a dodge: each is one formula or one library flag, identical to write, test, and reverse, so no rung buys its value with more of the reader's time.sleep = random(0, min(cap, base * 2^attempt))) - lowest total client work and lowest server load, at slightly more total time to completion.Retry → CircuitBreaker → RateLimiter → Bulkhead.samber/developer-platform-skills@api-rate-limit-policy territory.Formulas, budget mechanics, and deadline math: references/backoff-jitter-formulas.md. Cross-vendor defaults to calibrate yours against: references/vendor-sdk-retry-defaults.md.
Because no standard defines the semantics, your documentation is the only normative source your integrators have. Document, per the audience split above:
1s → 2s → 4s → 8s capped at 30s.Anti-pattern checklist - each is a direct audit finding:
SETNX lock with no fencing token as the claim.436d806c08ec