Idempotency keys
Network retries are a fact of life on serverless platforms, behind load balancers, and on flaky mobile connections. For unsafe methods — POST, PUT, PATCH, DELETE — a blind retry can charge a card twice or create a duplicate order. As of 0.37.0 the idempotency() middleware gives those requests an exactly-once guarantee: the client sends a unique Idempotency-Key header, and DaloyJS makes sure the side effect runs at most once no matter how many times the request is replayed.
It is built-in and dependency-free — built on Web Crypto and the Web-standard Request/Response — so it runs unchanged on Node, Bun, Deno, Cloudflare Workers, and Vercel Edge. The behavior mirrors the IETF Idempotency-Key HTTP Header Field draft and the conventions used by major payment processors.
Quick start
Mount idempotency() ahead of the routes that need exactly-once semantics. That is all — clients opt in per request by sending an Idempotency-Key header.
How it works
For an applicable method that carries an Idempotency-Key header, the middleware fingerprints the request (method + path + body) and consults a pluggable store:
- First request — the handler runs normally; the final response is captured and persisted under the key for
ttlSeconds. - Identical retry (same key, same fingerprint, original completed) — the stored response is replayed byte-for-byte with an
Idempotency-Replayed: trueheader. The handler does not run again. - Retry while the first is still in flight — a
409 Conflictis returned (withCache-Control: no-store) so the client backs off instead of racing. - Same key, different body — a
422 Unprocessable Contentis returned. A key is permanently bound to the first payload it was used with.
Responses that are not safe to cache are never stored, and the reservation is released so the client can retry: server errors (5xx by default, see cacheableStatus) and responses larger than maxResponseBytes(1 MiB by default).
Options
Pluggable stores
The default MemoryIdempotencyStore is process-local — perfect for tests and single-instance deployments. For a multi-instance or serverless fleet, supply a shared backend by implementing IdempotencyStore. The contract mirrors SessionStore and the rate-limit store: the one rule is that reserve()must be atomic (“set if absent”), the exact SET key value NX semantics of Redis, so two concurrent requests cannot both win the reservation.
Client usage
Clients generate a unique key per logical operation (a UUID is ideal) and reuse it across retries of that same operation:
Security notes
- Keys are validated up front: empty, over-long (
maxKeyLength), or non-printable keys are rejected with400 Bad Requestbefore any store lookup. - Conflict and reuse responses (
409,422) carryCache-Control: no-storeso a shared cache cannot mask them. - Server errors are never cached, so a transient
5xxdoes not poison the key — the client can safely retry. - The stored body is capped by
maxResponseBytesto bound memory growth from large replies.