
멱등성(Idempotency): 중복 요청 안전하게 처리
멱등성의 개념과 구현 방법을 경험을 통해 이해한 과정

멱등성의 개념과 구현 방법을 경험을 통해 이해한 과정
AI와 딥러닝은 왜 CPU를 버리고 GPU를 선택했을까요? ALU 구조 차이부터 CUDA 메모리 계층, 그래픽 API(Vulakn/DirectX), 그리고 생성형 AI의 원리까지 하드웨어 가속의 모든 것을 다룹니다.

데이터베이스 샤딩의 개념과 대규모 트래픽 처리를 경험을 통해 이해한 과정

공개 API를 운영하다 보면 예상치 못한 대량 요청에 시달릴 수 있다. Rate Limiting과 API Key 관리로 API를 보호하는 방법을 정리했다.

admin/user 두 역할로 시작했는데, 요구사항이 복잡해지면서 RBAC만으로 부족해졌다. ABAC까지 고려한 권한 설계를 정리했다.

결제 API를 만들고 있었습니다. 테스트 중에 네트워크가 불안정해서 같은 결제 요청이 두 번 들어왔고, 사용자에게 이중 과금이 발생했습니다. 이건 큰 문제였죠. "어떻게 하면 같은 요청이 여러 번 와도 한 번만 처리할 수 있을까?"
이 질문이 저를 멱등성(Idempotency)이라는 개념으로 이끌었습니다. 처음엔 "그냥 중복 체크하면 되는 거 아니야?"라고 생각했는데, 알고 보니 훨씬 깊은 개념이었습니다.
가장 혼란스러웠던 부분은 "왜 GET은 멱등하고 POST는 아닌가?"였습니다. 둘 다 HTTP 메서드인데 뭐가 다른 걸까?
또 다른 혼란은 "Idempotency Key를 어디에 저장해야 하는가?"였습니다. 데이터베이스? 메모리? Redis?
그리고 "얼마나 오래 저장해야 하는가?"도 궁금했습니다. 영구적으로? 아니면 일정 시간 후 삭제?
이해하는 데 결정적이었던 비유는 "전등 스위치"였습니다.
멱등한 동작 = 전등 끄기:이 비유로 이해했습니다. 멱등성은 "같은 동작을 여러 번 해도 결과가 동일한 성질"이라는 것을. 그리고 이게 왜 분산 시스템에서 중요한지도 깨달았죠.
멱등성(Idempotency)은 같은 요청을 여러 번 수행해도 결과가 동일한 성질을 말합니다. 수학에서 온 개념인데, 컴퓨터 과학에서는 주로 API 설계와 분산 시스템에서 중요하게 다뤄집니다.
네트워크는 불안정합니다. 요청을 보냈는데 응답이 안 오면, 클라이언트는 재시도를 합니다. 이때 서버가 이미 요청을 처리했다면? 멱등성이 없으면 중복 처리가 발생합니다.
HTTP 메서드는 설계상 멱등성이 정해져 있습니다.
| 메서드 | 멱등성 | 설명 |
|---|---|---|
| GET | ✅ | 조회만 하므로 몇 번을 호출해도 서버 상태가 변하지 않음 |
| PUT | ✅ | 전체 교체이므로 같은 데이터로 여러 번 교체해도 결과는 같음 |
| DELETE | ✅ | 이미 삭제된 리소스를 또 삭제해도 결과는 같음 (404 또는 성공) |
| POST | ❌ | 생성 요청이므로 매번 새로운 리소스가 생성됨 |
| PATCH | ❌ | 부분 수정이므로 연산에 따라 결과가 달라질 수 있음 |
// 몇 번을 호출해도 서버 상태는 변하지 않음
GET /users/123
GET /users/123
GET /users/123
// 결과: 항상 같은 사용자 정보 반환
// 같은 데이터로 여러 번 교체해도 결과는 같음
PUT /users/123
{
"name": "John",
"email": "john@example.com"
}
// 다시 호출
PUT /users/123
{
"name": "John",
"email": "john@example.com"
}
// 결과: 사용자 정보는 동일하게 유지됨
// 첫 번째 호출: 삭제 성공 (200 OK)
DELETE /users/123
// 두 번째 호출: 이미 없음 (404 Not Found 또는 200 OK)
DELETE /users/123
// 결과: 어쨌든 리소스는 없는 상태로 동일
// 첫 번째 호출: 주문 1 생성
POST /orders
{
"product": "iPhone",
"quantity": 1
}
// 응답: { "id": 1, "product": "iPhone" }
// 두 번째 호출: 주문 2 생성 (중복!)
POST /orders
{
"product": "iPhone",
"quantity": 1
}
// 응답: { "id": 2, "product": "iPhone" }
// 문제: 같은 상품이 두 번 주문됨!
POST는 기본적으로 비멱등하지만, Idempotency Key를 사용하면 멱등하게 만들 수 있습니다.
클라이언트가 요청할 때 고유한 키를 함께 보냅니다. 서버는 이 키를 기록해두고, 같은 키로 요청이 다시 오면 이전 결과를 반환합니다.
// 클라이언트
POST /orders
Headers:
Idempotency-Key: "order-2024-abc-123"
Body:
{ "product": "iPhone", "quantity": 1 }
// 서버: 처음 보는 키 → 주문 처리
// 응답: { "id": 1, "product": "iPhone" }
// 네트워크 문제로 재시도
POST /orders
Headers:
Idempotency-Key: "order-2024-abc-123" // 같은 키!
Body:
{ "product": "iPhone", "quantity": 1 }
// 서버: 이미 본 키 → 이전 결과 반환
// 응답: { "id": 1, "product": "iPhone" } // 중복 주문 방지!
const express = require('express');
const app = express();
// 메모리에 저장 (실제로는 Redis 사용 권장)
const processedKeys = new Map();
app.post('/orders', (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
// Idempotency Key가 없으면 에러
if (!idempotencyKey) {
return res.status(400).json({ error: 'Idempotency-Key header required' });
}
// 이미 처리된 요청인지 확인
if (processedKeys.has(idempotencyKey)) {
const cachedResult = processedKeys.get(idempotencyKey);
console.log('Returning cached result for key:', idempotencyKey);
return res.status(200).json(cachedResult);
}
// 새로운 요청 처리
const order = {
id: Date.now(),
product: req.body.product,
quantity: req.body.quantity,
createdAt: new Date()
};
// 결과 저장
processedKeys.set(idempotencyKey, order);
res.status(201).json(order);
});
app.listen(3000);
메모리에 저장하면 서버가 재시작될 때 데이터가 사라집니다. 실제로는 Redis를 사용합니다.
const express = require('express');
const redis = require('redis');
const app = express();
const redisClient = redis.createClient();
app.post('/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ error: 'Idempotency-Key required' });
}
const cacheKey = `idempotency:${idempotencyKey}`;
// Redis에서 확인
const cached = await redisClient.get(cacheKey);
if (cached) {
return res.status(200).json(JSON.parse(cached));
}
// 주문 처리
const order = await createOrder(req.body);
// Redis에 저장 (24시간 TTL)
await redisClient.setEx(
cacheKey,
86400, // 24시간
JSON.stringify(order)
);
res.status(201).json(order);
});
클라이언트가 UUID를 생성해서 보냅니다.
// 클라이언트 (브라우저)
import { v4 as uuidv4 } from 'uuid';
async function createOrder(orderData) {
const idempotencyKey = uuidv4(); // "a3bb189e-8bf9-3888-9912-ace4e6543002"
const response = await fetch('/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(orderData)
});
return response.json();
}
Idempotency Key를 영구적으로 저장하면 메모리가 부족해집니다. 적절한 TTL을 설정하세요.
201 Created200 OK (이미 생성된 리소스 반환)주문 생성과 Idempotency Key 저장을 하나의 트랜잭션으로 처리하세요.
app.post('/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
// 트랜잭션 시작
const session = await mongoose.startSession();
session.startTransaction();
try {
// 중복 체크
const existing = await IdempotencyKey.findOne({ key: idempotencyKey }).session(session);
if (existing) {
await session.abortTransaction();
return res.status(200).json(existing.result);
}
// 주문 생성
const order = await Order.create([req.body], { session });
// Idempotency Key 저장
await IdempotencyKey.create([{
key: idempotencyKey,
result: order[0],
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
}], { session });
await session.commitTransaction();
res.status(201).json(order[0]);
} catch (error) {
await session.abortTransaction();
res.status(500).json({ error: error.message });
} finally {
session.endSession();
}
});
Stripe(결제 서비스)는 모든 POST 요청에 Idempotency Key를 지원합니다.
const stripe = require('stripe')('sk_test_...');
// 결제 생성
const payment = await stripe.paymentIntents.create(
{
amount: 2000,
currency: 'usd',
},
{
idempotencyKey: 'payment-2024-abc-123' // 같은 키로 재시도해도 중복 결제 안 됨
}
);
Idempotency Key는 전역적으로 고유해야 합니다. 단순히 주문 ID를 사용하면 안 됩니다.
// ❌ 나쁜 예: 주문 ID 사용
const idempotencyKey = `order-${orderId}`; // 다른 사용자가 같은 ID 사용 가능
// ✅ 좋은 예: UUID 사용
const idempotencyKey = uuidv4(); // 전역적으로 고유
주문 생성은 성공했는데 Idempotency Key 저장이 실패하면? 트랜잭션을 사용해야 합니다.
// ❌ 나쁜 예: 트랜잭션 없음
const order = await createOrder(req.body); // 성공
await saveIdempotencyKey(key, order); // 실패 → 중복 주문 발생 가능
// ✅ 좋은 예: 트랜잭션 사용
const session = await mongoose.startSession();
session.startTransaction();
try {
const order = await createOrder(req.body, session);
await saveIdempotencyKey(key, order, session);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
}
같은 Idempotency Key로 동시에 두 요청이 들어오면? 락(Lock)을 사용해야 합니다.
const redis = require('redis');
const redisClient = redis.createClient();
app.post('/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
const lockKey = `lock:${idempotencyKey}`;
// 락 획득 시도 (5초 타임아웃)
const lock = await redisClient.set(lockKey, '1', {
NX: true, // 키가 없을 때만 설정
EX: 5 // 5초 후 자동 삭제
});
if (!lock) {
// 다른 요청이 이미 처리 중
return res.status(429).json({ error: 'Request already in progress' });
}
try {
// 주문 처리
const order = await createOrder(req.body);
await saveIdempotencyKey(idempotencyKey, order);
res.status(201).json(order);
} finally {
// 락 해제
await redisClient.del(lockKey);
}
});
멱등성은 재시도 전략과 함께 사용됩니다.
async function createOrderWithRetry(orderData, maxRetries = 3) {
const idempotencyKey = uuidv4();
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(orderData)
});
if (response.ok) {
return await response.json();
}
// 5xx 에러만 재시도
if (response.status >= 500) {
const delay = Math.pow(2, attempt) * 1000; // 1초, 2초, 4초
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// 4xx 에러는 재시도 안 함
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (attempt === maxRetries - 1) {
throw error;
}
}
}
}
제가 실제로 멱등성을 적용한 프로젝트들입니다.
문제: 네트워크 타임아웃으로 인한 중복 결제가 한 달에 10건 이상 발생
해결: Idempotency Key 도입
결과: 중복 결제 0건, CS 문의 90% 감소
문제: 사용자가 "주문하기" 버튼을 여러 번 클릭하여 중복 주문 발생
해결:
결과: 중복 주문 완전 차단
문제: 네트워크 재시도로 같은 메시지가 여러 번 전송
해결:
결과: 중복 메시지 0건
세계 최고의 결제 기업 Stripe는 멱등성을 어떻게 구현했을까요? 엔지니어링 블로그를 분석해보면 흥미로운 디테일이 있습니다.
많이 혼동하는 개념입니다.
POST는 기본적으로 둘 다 아닙니다. 안전하지도 않고(리소스 생성), 멱등하지도 않죠(매번 생성). 그래서 우리가 멱등성 키로 "인위적인 멱등성"을 부여하는 것입니다.
멱등성은 분산 시스템에서 필수적인 개념입니다. 네트워크는 언제나 불안정하고, 재시도는 피할 수 없습니다. 멱등성을 보장하지 않으면 중복 처리로 인한 심각한 문제가 발생할 수 있습니다.
저는 이 개념을 이해하고 나서, 모든 중요한 POST API에 Idempotency Key를 적용했습니다. 그 결과 네트워크 재시도로 인한 중복 처리 문제가 완전히 사라졌습니다. 사용자 경험도 좋아졌고, 운영 부담도 크게 줄었죠.
특히 결제 API에서 효과가 컸습니다. 이전에는 네트워크 타임아웃으로 인한 중복 결제 문의가 한 달에 10건 이상 들어왔는데, Idempotency Key를 도입한 후로는 단 한 건도 발생하지 않았습니다. 고객 만족도도 올라가고, CS 팀의 업무 부담도 줄어들었습니다.
I was building a payment API. During testing, the network was unstable and the same payment request came in twice, causing double charging to the user. This was a serious problem. "How can I process a request only once even if it comes multiple times?"
This question led me to the concept of Idempotency. At first I thought "Can't I just check for duplicates?" but it turned out to be a much deeper concept.
The most confusing part was "Why is GET idempotent but POST isn't?" Both are HTTP methods, so what's the difference?
Another confusion was "Where should I store the Idempotency Key?" Database? Memory? Redis?
And "How long should I store it?" Permanently? Or delete after a certain time?
The decisive analogy was "light switch."
Idempotent action = Turning off light:This analogy helped me understand. Idempotency is "the property where repeating the same action produces the same result." And I realized why this is crucial in distributed systems.
Idempotency means performing the same request multiple times produces the same result. It's a concept from mathematics, but in computer science it's mainly important in API design and distributed systems.
Networks are unstable. When you send a request and don't get a response, the client retries. What if the server already processed the request? Without idempotency, duplicate processing occurs.
HTTP methods have designed-in idempotency.
| Method | Idempotent | Description |
|---|---|---|
| GET | ✅ | Only reads, so server state doesn't change no matter how many calls |
| PUT | ✅ | Full replacement, so replacing with same data multiple times has same result |
| DELETE | ✅ | Deleting already-deleted resource has same result (404 or success) |
| POST | ❌ | Creation request, so each call creates new resource |
| PATCH | ❌ | Partial update, so result can vary depending on operation |
// No matter how many times called, server state doesn't change
GET /users/123
GET /users/123
GET /users/123
// Result: Always returns same user info
// Replacing with same data multiple times has same result
PUT /users/123
{
"name": "John",
"email": "john@example.com"
}
// Call again
PUT /users/123
{
"name": "John",
"email": "john@example.com"
}
// Result: User info remains identical
// First call: Delete success (200 OK)
DELETE /users/123
// Second call: Already gone (404 Not Found or 200 OK)
DELETE /users/123
// Result: Either way, resource is in non-existent state
// First call: Create order 1
POST /orders
{
"product": "iPhone",
"quantity": 1
}
// Response: { "id": 1, "product": "iPhone" }
// Second call: Create order 2 (duplicate!)
POST /orders
{
"product": "iPhone",
"quantity": 1
}
// Response: { "id": 2, "product": "iPhone" }
// Problem: Same product ordered twice!
POST is non-idempotent by default, but can be made idempotent using an Idempotency Key.
Client sends a unique key with the request. Server records this key, and if a request comes again with the same key, returns the previous result.
// Client
POST /orders
Headers:
Idempotency-Key: "order-2024-abc-123"
Body:
{ "product": "iPhone", "quantity": 1 }
// Server: First time seeing this key → Process order
// Response: { "id": 1, "product": "iPhone" }
// Retry due to network issue
POST /orders
Headers:
Idempotency-Key: "order-2024-abc-123" // Same key!
Body:
{ "product": "iPhone", "quantity": 1 }
// Server: Already seen this key → Return previous result
// Response: { "id": 1, "product": "iPhone" } // Duplicate order prevented!
const express = require('express');
const app = express();
// Store in memory (Redis recommended for production)
const processedKeys = new Map();
app.post('/orders', (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
// Error if no Idempotency Key
if (!idempotencyKey) {
return res.status(400).json({ error: 'Idempotency-Key header required' });
}
// Check if already processed
if (processedKeys.has(idempotencyKey)) {
const cachedResult = processedKeys.get(idempotencyKey);
console.log('Returning cached result for key:', idempotencyKey);
return res.status(200).json(cachedResult);
}
// Process new request
const order = {
id: Date.now(),
product: req.body.product,
quantity: req.body.quantity,
createdAt: new Date()
};
// Save result
processedKeys.set(idempotencyKey, order);
res.status(201).json(order);
});
app.listen(3000);
Storing in memory loses data when server restarts. In production, use Redis.
const express = require('express');
const redis = require('redis');
const app = express();
const redisClient = redis.createClient();
app.post('/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ error: 'Idempotency-Key required' });
}
const cacheKey = `idempotency:${idempotencyKey}`;
// Check Redis
const cached = await redisClient.get(cacheKey);
if (cached) {
return res.status(200).json(JSON.parse(cached));
}
// Process order
const order = await createOrder(req.body);
// Store in Redis (24-hour TTL)
await redisClient.setEx(
cacheKey,
86400, // 24 hours
JSON.stringify(order)
);
res.status(201).json(order);
});
Client generates UUID and sends it.
// Client (browser)
import { v4 as uuidv4 } from 'uuid';
async function createOrder(orderData) {
const idempotencyKey = uuidv4();
const response = await fetch('/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(orderData)
});
return response.json();
}
Storing Idempotency Keys permanently causes memory shortage. Set appropriate TTL.
201 Created200 OK (return already-created resource)Process order creation and Idempotency Key storage in one transaction.
app.post('/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
// Start transaction
const session = await mongoose.startSession();
session.startTransaction();
try {
// Check for duplicate
const existing = await IdempotencyKey.findOne({ key: idempotencyKey }).session(session);
if (existing) {
await session.abortTransaction();
return res.status(200).json(existing.result);
}
// Create order
const order = await Order.create([req.body], { session });
// Save Idempotency Key
await IdempotencyKey.create([{
key: idempotencyKey,
result: order[0],
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
}], { session });
await session.commitTransaction();
res.status(201).json(order[0]);
} catch (error) {
await session.abortTransaction();
res.status(500).json({ error: error.message });
} finally {
session.endSession();
}
});
Idempotency Key must be globally unique. Don't simply use order ID.
// ❌ Bad: Using order ID
const idempotencyKey = `order-${orderId}`; // Different users can have same ID
// ✅ Good: Using UUID
const idempotencyKey = uuidv4(); // Globally unique
What if order creation succeeds but Idempotency Key storage fails? Use transactions.
// ❌ Bad: No transaction
const order = await createOrder(req.body); // Success
await saveIdempotencyKey(key, order); // Failure → Duplicate order possible
// ✅ Good: Using transaction
const session = await mongoose.startSession();
session.startTransaction();
try {
const order = await createOrder(req.body, session);
await saveIdempotencyKey(key, order, session);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
}
What if two requests come simultaneously with same Idempotency Key? Use locks.
const redis = require('redis');
const redisClient = redis.createClient();
app.post('/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
const lockKey = `lock:${idempotencyKey}`;
// Try to acquire lock (5 second timeout)
const lock = await redisClient.set(lockKey, '1', {
NX: true, // Set only if key doesn't exist
EX: 5 // Auto-delete after 5 seconds
});
if (!lock) {
// Another request already processing
return res.status(429).json({ error: 'Request already in progress' });
}
try {
// Process order
const order = await createOrder(req.body);
await saveIdempotencyKey(idempotencyKey, order);
res.status(201).json(order);
} finally {
// Release lock
await redisClient.del(lockKey);
}
});
Idempotency is used together with retry strategies.
async function createOrderWithRetry(orderData, maxRetries = 3) {
const idempotencyKey = uuidv4();
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(orderData)
});
if (response.ok) {
return await response.json();
}
// Retry only on 5xx errors
if (response.status >= 500) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// Don't retry on 4xx errors
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (attempt === maxRetries - 1) {
throw error;
}
}
}
}
Stripe (payment service) supports Idempotency Key for all POST requests.
const stripe = require('stripe')('sk_test_...');
const payment = await stripe.paymentIntents.create(
{
amount: 2000,
currency: 'usd',
},
{
idempotencyKey: 'payment-2024-abc-123' // No duplicate payment even with retry
}
);
Payment systems are especially vulnerable to duplicate processing. Network timeouts are common, and users often retry failed payments. Without idempotency:
With idempotency:
function isValidIdempotencyKey(key) {
// UUID v4 format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(key);
}
app.post('/orders', (req, res) => {
const key = req.headers['idempotency-key'];
if (!key || !isValidIdempotencyKey(key)) {
return res.status(400).json({
error: 'Valid Idempotency-Key header required (UUID v4 format)'
});
}
// Process order...
});
Store a hash of the request body to detect if the same key is used with different data.
const crypto = require('crypto');
function hashRequest(body) {
return crypto.createHash('sha256').update(JSON.stringify(body)).digest('hex');
}
app.post('/orders', async (req, res) => {
const key = req.headers['idempotency-key'];
const requestHash = hashRequest(req.body);
const cached = await redis.get(`idempotency:${key}`);
if (cached) {
const cachedData = JSON.parse(cached);
// Check if request body matches
if (cachedData.requestHash !== requestHash) {
return res.status(422).json({
error: 'Idempotency key reused with different request body'
});
}
return res.status(200).json(cachedData.result);
}
// Process order...
const order = await createOrder(req.body);
await redis.setEx(`idempotency:${key}`, 86400, JSON.stringify({
result: order,
requestHash: requestHash
}));
res.status(201).json(order);
});
Track metrics to understand retry patterns.
// Metrics tracking
const metrics = {
totalRequests: 0,
duplicateRequests: 0,
uniqueKeys: new Set()
};
app.post('/orders', async (req, res) => {
const key = req.headers['idempotency-key'];
metrics.totalRequests++;
const cached = await redis.get(`idempotency:${key}`);
if (cached) {
metrics.duplicateRequests++;
console.log(`Duplicate rate: ${(metrics.duplicateRequests / metrics.totalRequests * 100).toFixed(2)}%`);
return res.status(200).json(JSON.parse(cached));
}
metrics.uniqueKeys.add(key);
// Process order...
});
Idempotency is essential in distributed systems. Networks are always unstable, and retries are unavoidable. Without idempotency guarantees, serious problems from duplicate processing can occur.
Key Takeaways:After understanding this concept, I applied Idempotency Key to all important POST APIs. As a result, duplicate processing problems from network retries completely disappeared. User experience improved, and operational burden significantly reduced.
Especially effective for payment APIs. Previously, we received 10+ duplicate payment inquiries per month due to network timeouts. After introducing Idempotency Key, not a single case occurred. Customer satisfaction increased, and CS team workload decreased.
The implementation was straightforward: add UUID generation on the client, validate and check keys on the server, and store results in Redis with appropriate TTL. The benefits far outweighed the implementation cost.
Without TTL, your Redis will fill up with old idempotency keys. Always set appropriate expiration.
Don't use predictable IDs like order-1, order-2. Use UUIDs to prevent key collisions across different users.
If the same idempotency key is used with different request bodies, that's likely a client bug. Detect and reject it.
Always use database transactions to ensure atomicity between business logic and idempotency key storage.
How does Stripe, the gold standard of payments, handle this? Their engineering blog reveals fascinating details:
Commonly confused concepts.
POST is neither by default. It changes state (Unsafe) and creates new things each time (Non-idempotent). That's why we artificially enforce idempotency using keys.