Idempotency Key API Design
Organizing key scope, TTL, and response reuse policies to safely handle duplicate requests

Introduction
In functions where duplicate execution is critical, such as payment, ordering, and point accumulation, data consistency problems begin as soon as network retries are allowed. Overlapping client retries, gateway timeouts, and message broker retransmissions can cause the same request to be processed multiple times. This article defines Idempotency-Key as an API contract and summarizes a practical guide for designing storage/TTL/response cache.

Problem definition
Redundant processing issues are usually discovered after a major failure, but the cause can be predicted in advance. If you see any of the following signs, you should immediately improve your design:
- There is no key generation rule to distinguish identical requests, so retries and new requests cannot be distinguished.
- The success response is not saved, so the results may vary upon retry.
- Since the key storage TTL is shorter than the business guarantee time, duplicate reflection occurs in late retries.
Idempotency is not a simple key check, but a state machine. Separating IN_PROGRESS, SUCCEEDED, and FAILED_RETRIABLE makes it easier to respond to failures.
Key concepts
| perspective | Design criteria | Verification points |
|---|---|---|
| key generation | Client generates UUID per unit operation | Duplication rate and conflict rate |
| save state | Hash request body + store response body | Response consistency across retries |
| TTL policy | Stay above guaranteed business hours | Whether to block late retries |
| Observability | Per-key log/metric collection | Duplicate request detection time |
The key is to specify the contract at the API level. If only the server is implemented and the client guide is omitted, the duplication problem will recur.
Code Example 1: Idempotency Repository Interface
export type IdempotencyRecord = {
key: string;
requestHash: string;
status: "IN_PROGRESS" | "SUCCEEDED" | "FAILED_RETRIABLE";
responseCode?: number;
responseBody?: string;
expiresAt: Date;
};
export interface IdempotencyStore {
begin(record: Omit<IdempotencyRecord, "status">): Promise<"CREATED" | "EXISTS">;
complete(key: string, responseCode: number, responseBody: string): Promise<void>;
find(key: string): Promise<IdempotencyRecord | null>;
}
Code example 2: API handler processing routine
export async function createOrderHandler(req: Request) {
const key = req.headers.get("Idempotency-Key");
if (!key) return new Response("Missing Idempotency-Key", { status: 400 });
const body = await req.json();
const requestHash = await sha256(JSON.stringify(body));
const started = await idempotencyStore.begin({ key, requestHash, expiresAt: plusHours(24) });
if (started === "EXISTS") {
const record = await idempotencyStore.find(key);
if (!record) return new Response("Conflict", { status: 409 });
return new Response(record.responseBody, { status: record.responseCode ?? 202 });
}
const result = await orderUseCase.execute(body);
await idempotencyStore.complete(key, 201, JSON.stringify(result));
return new Response(JSON.stringify(result), { status: 201 });
}
Architecture flow
Tradeoffs
- Storage space is increased for response reuse, but it is cheaper than payment/order consistency costs.
- It is safe to keep the key TTL long, but storage costs and GDPR deletion policies must be considered together.
- Incorporating request hash verification increases security but increases CPU cost.
Cleanup
Idempotency-Key is a fail-safe in a retry-allowing system. By designing key generation rules, storage states, and response reuse policies together, business consistency can be maintained even in failure situations.
Image source
- Cover: source link
- License: CC BY-SA 4.0 / Author: Papapep
- Note: After downloading the free license image from Wikimedia Commons, it was optimized to JPG at 1600px.