An idempotency key lets a client retry one logical operation without making the server perform it again. For a production API, the useful pattern is to bind the key to the caller and request, record the operation’s state, and replay the saved status and body after success. Redis can help coordinate that record, but a Redis key alone cannot make a database update or external side effect exactly once.
What is an idempotency key?
An idempotency key is a client-supplied identifier for one logical operation. If a client sends a request, loses the response, and retries with the same key, the server can recognize the retry as belonging to the earlier operation. It should not generate a new key for each network attempt.
As Stripe’s API documentation puts it: “The API supports idempotency for safely retrying requests without accidentally performing the same operation twice.” In Stripe’s documented model, later requests using the same key receive the saved status and body. That is one provider’s contract, not a universal rule for every API.
Idempotency is particularly useful for operations such as creating a payment, placing an order, or submitting a job, where a client cannot tell whether a timed-out request reached the server. It does not make every HTTP method or every side effect inherently safe; the API must define what the key means and how it handles each outcome.
#1 Best Overall
How do I prevent duplicate POST requests?
Define a contract between client and server before choosing the Redis commands. A client creates a sufficiently random key once per logical operation and sends it on retries. The server scopes that key to the principal and operation—for example, tenant, HTTP method, and route—so unrelated callers or endpoints cannot collide.
For each scoped key, retain a request fingerprint and an operation state. A canonical representation matters: hashing arbitrary JSON serialization can produce different fingerprints for semantically identical objects if property order differs. Include all fields that materially affect the operation, and use a consistent canonicalization scheme.
- Same key and matching request, completed: return the stored status and body rather than executing the operation again.
- Same key and matching request, still running: return a documented in-progress response, such as a conflict with retry guidance, or wait under an explicit policy. Do not run the side effect in parallel.
- Same key and different request: reject the reuse. Stripe documents comparing parameters and reporting an error for mismatches; an API should likewise make its mismatch behavior explicit.
- Validation fails before work begins: decide whether to retain the key. Stripe says it does not save an idempotent result when validation fails, allowing a corrected request to be submitted.
- Work fails: distinguish a known failure before any side effect from an uncertain outcome after one may have occurred. Define whether the key remains retryable, returns a stored failure, or requires reconciliation.
These choices are part of the API contract. A generic Redis lock tutorial cannot supply them: a lock identifies a current claimant, while an idempotency engine must also match requests and recover the original result.
How does Redis SET NX EX fit?
Redis can atomically create a key only if it does not already exist and set its expiry in the same command: SET key value NX EX seconds. The Redis SET command documentation describes NX as the “only set the key if it does not already exist” condition, and documents that a successful set returns OK; when the condition fails, the result is null. The expiry prevents an abandoned in-progress claim from living forever.
Rank #2
This is a useful claim primitive, not a complete response-replay engine. It does not compare request parameters, save a completed HTTP response, or coordinate a database transaction. A production design needs explicit state transitions and a recovery plan for the boundary between the claim, business work, and response persistence.
A Node.js and Redis implementation pattern
The following focused example shows the claim and completion operations using Node.js 25.9.0 and a Redis client exposing sendCommand and eval. Configure the in-progress lease and completed-record retention for your own retry contract; the values are deliberately configuration inputs, not universal recommendations. The request canonicalizer is also application-specific and must produce stable output.
import { createHash, randomUUID } from 'node:crypto';
const IN_PROGRESS_SECONDS = Number(process.env.IDEMPOTENCY_LEASE_SECONDS);
const RETENTION_SECONDS = Number(process.env.IDEMPOTENCY_RETENTION_SECONDS);
function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}
// canonicalize() must be a stable, application-defined representation.
function makeRecordKey({ tenantId, method, route, clientKey }) {
const scope = `${tenantId} ${method} ${route} ${clientKey}`;
return `idem:${sha256(scope)}`;
}
async function claim(redis, { key, requestHash }) {
const token = randomUUID();
const claimRecord = JSON.stringify({
state: 'in_progress',
requestHash,
token
});
const result = await redis.sendCommand([
'SET', key, claimRecord, 'NX', 'EX', String(IN_PROGRESS_SECONDS)
]);
if (result === 'OK') return { kind: 'claimed', token, claimRecord };
const current = await redis.sendCommand(['GET', key]);
if (current === null) return { kind: 'retry_claim' };
const record = JSON.parse(current);
if (record.requestHash !== requestHash) return { kind: 'mismatch' };
if (record.state === 'completed') {
return { kind: 'replay', status: record.status, body: record.body };
}
return { kind: 'in_progress' };
}
const COMPLETE_IF_OWNER = `
if redis.call('GET', KEYS[1]) ~= ARGV[1] then
return 0
end
redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[3])
return 1
`;
async function saveResponse(redis, { key, claimRecord, requestHash, status, body }) {
const completedRecord = JSON.stringify({
state: 'completed', requestHash, status, body
});
return redis.eval(COMPLETE_IF_OWNER, {
keys: [key],
arguments: [claimRecord, completedRecord, String(RETENTION_SECONDS)]
});
}
Generate a fresh random claim token for each attempt; Node’s crypto.randomUUID() uses a cryptographic pseudorandom number generator to create an RFC 4122 version 4 UUID. Node documents it as added in v14.17.0 and v15.6.0 in its Crypto API documentation. Stripe suggests V4 UUIDs or another random string with sufficient entropy and sets a 255-character maximum for keys accepted by its API. That limit is Stripe-specific, not a general HTTP standard.
Connect the helper to the request handler
- Validate before claiming. Perform syntax and business validation that can safely happen before work. This example follows a policy in which validation failures do not consume the key.
- Build the scoped record key and fingerprint. Include the tenant or account, method, route, and client key in the scope. Fingerprint the canonicalized material request inputs.
- Claim atomically. A successful
SET ... NX EX ...grants this request the in-progress record. On a lost race, read the current record and apply the contract: reject mismatches, replay completed results, or report in-progress status. If the key expired between the failed claim and the read, retry the claim a bounded number of times. - Perform the business operation once under its own correctness controls. Use the claim to prevent ordinary concurrent duplicates, but also make the database or downstream operation recoverable or idempotent.
- Persist the response conditionally. Save the status and body with an atomic transition that verifies the current record is still this request’s claim. If it is not, do not overwrite a newer owner’s state.
The example stores JSON response bodies; adapt serialization for the API’s response model, and avoid storing sensitive data unless its access, encryption, and retention are appropriate. A real handler must map mismatch, in_progress, replay, and claim races to documented HTTP behavior.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteRank #3
What happens with simultaneous requests?
The first successful conditional set wins the claim. A simultaneous request that uses the same scoped key should see the existing in-progress record and follow the API’s conflict or wait policy. It must not interpret “could not claim” as permission to run the operation anyway.
The lease is not proof that the first request stopped. If its work lasts beyond the in-progress TTL, a later retry may claim the expired key while the original is still running. Choose a lease that fits expected processing, and consider renewal or another coordination mechanism for long-running work. Any lease can still leave a failure window, so the business operation needs a separate safeguard.
Redis documents a simple SET NX lock pattern but cautions that it is discouraged for locking use cases in favor of Redlock; its command guidance also discusses random tokens and token-checked release so an expired lock holder cannot delete a newer holder’s lock. Those lock considerations do not turn a short-lived lock into an HTTP response store. Use the data model and recovery rules required by the API’s result-replay contract.
Where atomicity stops
The conditional claim and the later response save are separate events. Even if response persistence is atomic within Redis, that does not make a relational database update, payment provider call, or multi-step workflow atomic with Redis.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallRank #4
For example, the business side effect might succeed and the process might crash before the completed response is saved. A retry then sees an in-progress claim until it expires, or may eventually acquire a new claim; Redis cannot tell by itself whether the external work happened. Conversely, Redis could lose or fail over a record depending on deployment and persistence configuration while the business system retains the side effect.
- For database-owned operations, consider recording a unique operation identifier or idempotency key in the same database transaction as the business change. A uniqueness constraint can prevent a second application of that operation.
- For external services, pass a stable downstream idempotency identifier where supported, and provide a way to query or reconcile the outcome before retrying an uncertain call.
- For multi-step workflows, persist durable operation state and recovery actions appropriate to the system, rather than treating a Redis lease as a transaction coordinator.
The right recovery design depends on where the authoritative business state lives. Redis is useful for fast coordination and replay metadata, but a lock alone cannot guarantee exactly-once effects across Redis, a database, and an external service.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.How long should an idempotency key be kept?
Retention defines how long a retry can be recognized. Set the window based on realistic client retry behavior and the consequences of repeating the operation; publish it to API clients. If a record expires while a client still considers its retry valid, the same key may be treated as a new request.
Stripe says it may prune keys once they are at least 24 hours old; after pruning, reuse can be treated as a new request. That is Stripe’s policy, not a default every API should copy. Redis lets the application attach an expiry atomically to a set operation using options such as EX, as described in the SET command documentation.
Free tools Windows power users keep installed
One-click scans. No signup required.
Best Value
Usually the in-progress lease and completed-result retention serve different purposes: the lease recovers abandoned work, while the completed record preserves replay behavior. Define both deliberately. If a record is removed before the advertised retry window, the server no longer has the saved result to return.
Redis client reconnects can change the outcome
A Redis connection drop is not always equivalent to a command that never happened. The Redis Node.js production guidance warns that automatic reconnect behavior can queue commands while disconnected and later resend them; if a state-changing command reached Redis before the connection failed, replaying it may produce an incorrect result. See Redis’s Node.js production usage guidance.
Review the client’s offline-queue and retry settings for the specific commands in this flow. Redis documents disableOfflineQueue as a way to discard unexecuted queued commands, but disabling the queue is not universally correct: it changes availability and error behavior, and it does not resolve uncertainty about a command that may already have executed. The application needs bounded retries, clear handling of ambiguous outcomes, and business-level deduplication where a repeated command or side effect would be harmful.
API idempotency is not Redis Streams producer idempotency
Redis Streams has producer-side idempotent message production through XADD with IDMP or IDMPAUTO. Detection depends on retrying with the same idempotent ID, and tracking is producer-scoped. Those mechanics can help avoid duplicate stream entries, but they do not store and replay an HTTP status code and response body. See Redis’s Streams idempotency documentation.
| Approach | What it gives you | What it does not establish |
|---|---|---|
| API idempotency record | Request matching, operation state, and replay of a saved HTTP result when implemented as a complete contract. | Atomicity of a side effect in a separate database or service. |
Redis SET NX EX claim |
An atomic first claimant and an expiring key. | Completed response replay, mismatch semantics, or cross-system exactly-once effects. |
| Redis Streams producer idempotency | Producer-scoped deduplication of stream message production when the same ID is reused. | HTTP response persistence and replay. |
Choose Redis-backed request records when the service needs quick duplicate recognition and can tolerate the operational characteristics of its Redis deployment. Keep the system’s durable business data and recovery mechanism aligned with the system that owns the actual side effect.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

