
Introduction
In modern distributed systems, particularly in payment APIs, order processing, and transactional services, idempotency is an important principle that ensures the same operation produces only one effect, even when the same request is sent multiple times.
Idempotency is commonly used to handle retries caused by network failures, timeouts, or client-side connection issues. While it improves system reliability, an improper implementation can unintentionally introduce security vulnerabilities into web applications.
This article discusses how idempotency works, the security risks associated with poor implementations, and best practices for implementing it securely.
What Is Idempotency?
An operation is considered idempotent if a process executed multiple times results in the same final state as executing it once [1].
For example, a payment API may receive the following request:
POST /paymentIdempotency-Key: 9ef81b02-55yh-5r1a-0p2o-236af2sd9c
If the payment is successfully processed but the client loses the server response due to network timeout, the client may retry the request using the same idempotency key. Instead of creating a second payment, the server returns the previously stored response.
Idempotent vs Non-Idempotent HTTP Methods
REST defines how web requests are expected to behave. Some HTTP methods are designed to be safe and idempotent, so sending the same request multiple times does not change the outcome. Other methods are used to create new data and need extra protection to ensure retries do not cause duplicate actions.
Idempotent Methods (GET, PUT, DELETE)
RFC 9110 defines several standard methods that are inherently idempotent. Repeating an action on these methods should not change the state of the server beyond the initial request.
- GET: Retrieves data, doesn't change anything on the server. There is no problem when called a million times the resource remains the same.
- PUT: Replaces a resource entirely. Updating the same unique identifier e.g. email (
user@example.com) three times still results in the email being "user@example.com". - DELETE: Removing a resource will always result in the resource being removed, regardless of how many times the delete is invoked.
Non-Idempotent Methods (POST, PATCH)
Some HTTP methods are naturally more vulnerable to duplicate execution because they are designed to create or modify data rather than simply retrieve it.
- POST: Requests commonly create new resources. If the same request is processed multiple times without idempotency controls, it can lead to duplicate transactions, duplicate records, or repeated financial charges.
- PATCH: Requests apply partial modifications to existing resources. While they can be implemented in an idempotent manner, operations based on relative changes — such as "add 5 credits" or "increment a counter" — may produce different outcomes each time the request is replayed, increasing the risk of unintended data manipulation.
Idempotency and Website Security: What's the Connection?
Idempotency is not a primary feature for securing an API. It's not classified as a direct vulnerability like SQL Injection or Broken Authentication. However, idempotency helps prevent duplicate requests, inconsistent states, and mitigates the impact of replay attacks, which improves overall system robustness [4].
Security Risks
Replay Attacks
A replay attack occurs when an attacker captures a valid request and resends it to the server.
Consider the following request:
POST /transferIdempotency-Key: abc123
If an attacker obtains:
- the authentication token,
- the request payload,
- and the idempotency key,
they may attempt to resend the request.
If the server fails to:
- enforce expiration policies,
- validate ownership of the key,
- or verify the original request payload,
the attacker may be able to replay the transaction successfully.
Race Conditions
Race conditions occur when multiple requests are processed simultaneously before the idempotency record is stored.
Example:
if (!exists(key)) {createPayment();saveKey();}
Two concurrent requests may both pass the existence check before either request saves the key. As a result:
- duplicate payments may occur,
- duplicate orders may be created,
- account balances may become inconsistent.
Race conditions are one of the most challenging aspects of implementing idempotency in distributed systems [4].
Mitigation
Use:
- Database transactions.
- Distributed locking.
- Redis
SETNXoperations. - Unique database constraints.
Example:
CREATE UNIQUE INDEX idx_payment_keyON payments(idempotency_key);
This ensures only one transaction can succeed with a given key [4].
Key Collision
Weak idempotency key generation can lead to collisions.
Examples of poor implementations:
const key = userId;
or
const key = Date.now();
Predictable keys increase the risk that:
- multiple users share the same key,
- responses are mixed between users,
- sensitive information is exposed.
Mitigation
Generate cryptographically strong identifiers such as UUID v4:
import { randomUUID } from "crypto";const key = randomUUID();
Stripe recommends using sufficiently random keys with high entropy to minimize collision risks [2].
Payload Tampering
Consider the following initial request:
{"amount": 100}
with:
Idempotency-Key: abc123
An attacker later submits:
{"amount": 100000}
using the same key.
If the server only checks whether the key exists and ignores payload differences, unexpected behavior may occur.
Mitigation
Store:
idempotency_keyrequest_hashresponse_body
Then verify:
if (incomingHash !== storedHash)throw new Error("Payload mismatch");
This prevents attackers from reusing a valid key with altered request parameters [2].
Denial of Service
Attackers may intentionally generate millions of unique idempotency keys:
key1key2key3...
Each key consumes storage and processing resources.
Potential impacts include:
- Redis memory exhaustion.
- Database growth.
- Increased latency.
- Infrastructure cost escalation.
Mitigation
Implement:
- Rate limiting.
- Key expiration policies.
- Cache eviction strategies.
- Storage quotas.
- Monitoring and alerting.
These controls reduce the effectiveness of resource exhaustion attacks.
Broken Access Control
Suppose a system stores:
idempotency_keyresponse
and retrieves records using:
findByKey(key);
without validating ownership.
If an attacker discovers another user's key, they may retrieve responses belonging to that user.
This issue aligns with Broken Object Level Authorization (BOLA), which remains one of the most critical API security risks identified by OWASP.
Mitigation
Store records using:
(user_id, idempotency_key)
instead of:
idempotency_key
alone.
Every lookup should verify both the key and the authenticated user.
Bad Implementations of Idempotency
1. Only Implementing Redis Lock
const acquired = await redis.set(lockKey, "locked", {NX: true,EX: 5,});
Why? Because a Redis lock is not idempotency — it only guarantees mutual exclusion while the lock exists. For example:
Payment Request A│Acquire Lock│Create Payment│Lock expires│Payment Request B│Acquire Lock│Create Payment Again
There is nothing that remembers the payment has already been processed.
Evidence
The Stripe API documentation states that the server stores the result of the first request associated with an idempotency key and returns that stored result for subsequent requests with the same key — not merely a temporary lock.
Source: Stripe Documentation — Idempotent
2. Using Timestamp as Idempotency Key
const key = Date.now().toString();
Why is this bad? Imagine we processed a payment request, but the client doesn't get a response because of a request timeout. The client retries twice:
Retry #1 1990001 - Generate new payment processRetry #2 1990002 - Generate new payment process
This is not idempotency — the server has no way to associate these requests.
Evidence
Stripe recommends: "Use V4 UUIDs, or another random string with enough entropy."
3. Not Comparing the Request Payload
One common mistake is only checking the idempotency key and ignoring whether the request body is the same.
For example, the server stores:
Idempotency-Keyabc123
The first request is:
POST /paymentsIdempotency-Key: abc123
{"amount": 100}
The payment is processed successfully.
Later, another request is sent using the same idempotency key but with a different payload:
POST /paymentsIdempotency-Key: abc123
{"amount": 1000}
If the server only checks whether the key exists:
if (existingKey) {return storedResponse;}
it assumes both requests are identical, even though the payment amounts are different. We must compare the incoming request and the stored request when the existing key is the same.
if (stored.requestHash !== incoming.requestHash) {throw new ConflictException("Request payload does not match.");}
Evidence
Stripe follows this approach in its idempotency implementation: "The idempotency layer compares incoming parameters to those of the original request and errors if they're not the same."
Reference: Stripe Documentation — Idempotent
4. No User Ownership Validation
Imagine a database that stores only the key:
Keyabc123
and a server that retrieves records using:
findByKey(key)
with no user validation.
If another user discovers the key:
User A↓abc123↓User B↓abc123
User B receives User A's response.
Evidence
This falls under Broken Object Level Authorization (BOLA). OWASP API Security Top 10 identifies BOLA as the number one API security risk.
Source: https:
5. Never Expiring Keys
Suppose you never delete keys:
2024abc123↓2026Still exists
Problems:
- storage growth
- replay window increases
- cache memory consumption
Evidence
Stripe states: "You can remove keys from the system automatically after they're at least 24 hours old."
Source: https:
Point to Implement Idempotency Safely?
A secure implementation of idempotency should store the following fields:
idempotency_keyuser_idrequest_hashresponse_bodystatus_codecreated_atexpired_at
Use Unique Constraints
CREATE UNIQUE INDEX idx_user_keyON payment_requests(user_id, idempotency_key);
Apply Expiration Policies
Many payment providers recommend retaining keys for a limited period, commonly around 24 hours.
Verify Request Payloads
if (storedHash !== currentHash) return 409;
Execute Query Operation Within a Transaction
Queries should be applied within a transaction, for example using Prisma:
await prisma.$transaction(async (tx) => {//...your query here})
Track and Monitor Abuse Patterns
- High volumes of unique keys.
- Excessive retry attempts.
- Unusual replay activity.
- Suspicious cross-user access attempts.
Example Safe Implementation of Idempotency on Node.js
This example is based on a real project with a payment API. I applied several safe idempotency steps to prevent bugs, errors, and security risks.
Step 1: Create the Idempotency Model in Prisma
model Idempotency {id String @id @default(cuid())key String @unique()request_hash Stringresponse_body Jsonstatus_code Intsuccess Booleancreated_at DateTime @default(now())@@index([key])@@unique([id, key])@@map("idempotency")}
As mentioned before, we need to save the request hash to validate the request body later.
Step 2: Read the Idempotency Key and Hash the Request
const idempotencyKey = req.headers["idempotency-key"] as string;if (!idempotencyKey) throw new ResponseError(400, "Idempotency key is required");const requestBodyHash = crypto.createHash("sha256").update(JSON.stringify(req.body)).digest("hex");
Reading and validating the idempotency key — the idempotency key is sent from the client through the request header. Next, hash the request body using the SHA-256 hash method.
Step 3: Get and Validate Idempotency Data
// Step 1: Get Idempotency Keyconst idempotency = await this.idempotencyRepository.getIdempotency({idempotencyKey: payload.idempotencyKey,});// Step 2: Validate Idempotency Ownershipif (idempotency && (idempotency.result.user_id !== payload.userId))throw new ResponseError(409, "Invalid idempotency ownership");// Step 3: Validate Idempotency Requestif (idempotency && (idempotency.result.request_hash !== payload.requestBodyHash))throw new ResponseError(409, "Invalid idempotency request");// Return Available Response if idempotency key existsif (idempotency.result) return idempotency.result.response;
Validating Idempotency Ownership — Evidence
Case:
- First, I created the registration data with a generated idempotency key on the client.
- Then I changed the Authorization header to a different user and retried the request with the same generated idempotency key.

Validating Idempotency Request — Evidence
Case:
- First, I created the registration data with a generated idempotency key on the client.
- Then I changed the request body and retried the request with the same generated idempotency key.

Step 4: Redis Lock Implementation (Prevent Race Condition)
// Acquire Lockconst lockAcquired = await redisClient.set(`lock:${payload.idempotencyKey}`, "TRUE", {NX: true,EX: 30,});if (!lockAcquired) throw new ResponseError(409, "Request is already being processed");
A Redis lock is a temporary marker that tells your application, "This request is already being processed." When the first request arrives, it creates a lock in Redis. If another request with the same lock key arrives before the first one finishes, Redis rejects it because the lock already exists. This helps prevent multiple requests from executing the same business logic at the same time.
The lock is temporary because of the EX: 30 option, which automatically removes it after 30 seconds. This ensures the lock does not remain forever if the application crashes or fails to release it. However, a Redis lock is not idempotency — it only prevents concurrent processing. To safely handle retries after the original request has finished, you still need an idempotency record stored in your database.
Step 5: Implement the Transaction Query
const registerData: TournamentRegistration & { invoiceUrl?: string } = await this.prismaClient.$transaction(async (tx) => {//......... other query// Delete the Redis lock because the query is already committedawait redisClient.del(`lock:${payload.idempotencyKey}`);await this.idempotencyRepository.createIdempotency({idempotencyKey: payload.idempotencyKey,requestBodyHash: payload.requestBodyHash,responseBody: ApiResponseBuilder.created(registerQueryData, "Tournament registration created successfully") as object,statusCode: 201,success: true,}, tx);return {...registerQueryData,invoiceUrl: billing.invoiceUrl,};});return registerQueryData;
Result
Case:
First, make a request with:
- Idempotency key:
cmqys3af2000004lb0u48fowy - Authorization: JWT token
- Request body:
{"categoryId": 3,"redirectUrl": "https://app.example.com/payment/success","myOrderUrl": "https://app.example.com/my-orders","skillRating": 4.5,"ratingSystem": "NRTP"}First result is created successfully.
- Idempotency key:
Try to make the same request with the same idempotency key and user. The server returns the previously created response without creating a new one.

Checking the database confirms that only one record was created.

Conclusion
Idempotency is a critical design principle for modern APIs, especially in payment and transaction-based systems, because it ensures repeated requests produce a single consistent outcome. It helps systems stay reliable under retries caused by network issues or timeouts, but it must be implemented carefully — because a weak idempotency design can introduce serious security and integrity risks.
This article showed that poor idempotency implementations can lead to replay attacks, race conditions, key collisions, payload tampering, resource exhaustion, and broken access control (BOLA). These risks usually happen when systems only rely on simple checks (like "does the key exist?") without validating request payloads, enforcing ownership, applying expiration policies, and preventing concurrent execution.
A secure idempotency solution should store enough data to safely replay responses (key, user ownership, request hash, stored response, timestamps), enforce uniqueness constraints, compare payload hashes, and process requests inside transactions. When combined with rate limits, monitoring, and proper expiration, idempotency becomes not only a reliability feature but also an important layer that reduces abuse, prevents duplication, and protects users from unintended repeated actions.
References
- [1]Algoroq. Idempotency Explained: Designing Safe Retries in Distributed Systems. https:
/ / www.algoroq.io/ concepts/ idempotency/ - [2]Stripe Documentation. Idempotent Requests. https:
/ / docs.stripe.com/ api/ idempotent_ requests - [3]OWASP. OWASP API Security Top 10. https:
/ / owasp.org/ www- project- api- security/ - [4]CodeNotes. API Idempotency Keys: Prevent Duplicate Requests Safely. https:
/ / codenotes.tech/ blog/ api- idempotency- keys- prevent- duplicate- requests - [5]Stripe Engineering. Designing Robust and Predictable APIs with Idempotency. https:
/ / stripe.com/ blog/ idempotency - [6]Google Cloud. What is idempotency? https:
/ / cloud.google.com/ discover/ idempotency - [7]IETF. HTTP Semantics (RFC 9110). https:
/ / httpwg.org/ specs/ rfc9110.html#idempotent.methods



