idempotency
Ensures API actions can be repeated safely without causing duplicate side effects.
Install
mkdir -p .claude/skills/idempotency && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/4949" && unzip -o skill.zip -d .claude/skills/idempotency && rm skill.zipInstalls to .claude/skills/idempotency
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Implement idempotent API operations to safely handle retries and prevent duplicate processing. Use when building payment APIs, order systems, or any operation that must not be executed twice.Key capabilities
- →Implement idempotency keys for API requests
- →Check idempotency stores for existing records
- →Manage request locks during processing
- →Cache and return previous responses
- →Handle retries for idempotent operations
How it works
It uses a store to track request status via keys, preventing duplicate execution by locking and caching results.
Inputs & outputs
When to use idempotency
- →Prevent double charges in payments
- →Safely retry failed order submissions
- →Handle duplicate webhook events
About this skill
Idempotent API Operations
Safely handle retries without duplicate side effects.
When to Use This Skill
- Payment processing (charges, refunds)
- Order creation and fulfillment
- Any operation with side effects
- APIs that may be retried by clients
- Webhook handlers
What is Idempotency?
An operation is idempotent if executing it multiple times produces the same result as executing it once.
Request 1: POST /orders {item: "book"} → Order #123 created
Request 2: POST /orders {item: "book"} → Order #123 returned (not #124)
(same idempotency key)
Architecture
┌─────────────────────────────────────────────────────┐
│ Client Request │
│ Idempotency-Key: abc-123 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Check Idempotency Store │
│ │
│ Key exists? │
│ ├─ Yes, completed → Return cached response │
│ ├─ Yes, in-progress → Return 409 Conflict │
│ └─ No → Continue processing │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Lock & Process Request │
│ │
│ 1. Acquire lock (set key as "processing") │
│ 2. Execute operation │
│ 3. Store response │
│ 4. Return response │
└─────────────────────────────────────────────────────┘
TypeScript Implementation
Idempotency Store
// idempotency-store.ts
import { Redis } from 'ioredis';
interface IdempotencyRecord {
status: 'processing' | 'completed';
response?: {
statusCode: number;
body: unknown;
headers?: Record<string, string>;
};
createdAt: number;
completedAt?: number;
}
interface IdempotencyConfig {
redis: Redis;
keyPrefix?: string;
lockTtlMs?: number; // How long to hold processing lock
responseTtlMs?: number; // How long to cache completed responses
}
class IdempotencyStore {
private redis: Redis;
private keyPrefix: string;
private lockTtl: number;
private responseTtl: number;
constructor(config: IdempotencyConfig) {
this.redis = config.redis;
this.keyPrefix = config.keyPrefix || 'idempotency:';
this.lockTtl = config.lockTtlMs || 60000; // 1 minute
this.responseTtl = config.responseTtlMs || 86400000; // 24 hours
}
async get(key: string): Promise<IdempotencyRecord | null> {
const data = await this.redis.get(this.keyPrefix + key);
return data ? JSON.parse(data) : null;
}
async acquireLock(key: string): Promise<boolean> {
const record: IdempotencyRecord = {
status: 'processing',
createdAt: Date.now(),
};
// SET NX = only set if not exists
const result = await this.redis.set(
this.keyPrefix + key,
JSON.stringify(record),
'PX',
this.lockTtl,
'NX'
);
return result === 'OK';
}
async complete(
key: string,
response: IdempotencyRecord['response']
): Promise<void> {
const record: IdempotencyRecord = {
status: 'completed',
response,
createdAt: Date.now(),
completedAt: Date.now(),
};
await this.redis.set(
this.keyPrefix + key,
JSON.stringify(record),
'PX',
this.responseTtl
);
}
async release(key: string): Promise<void> {
await this.redis.del(this.keyPrefix + key);
}
}
export { IdempotencyStore, IdempotencyRecord, IdempotencyConfig };
Express Middleware
// idempotency-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { IdempotencyStore } from './idempotency-store';
interface IdempotencyOptions {
store: IdempotencyStore;
headerName?: string;
methods?: string[];
paths?: RegExp[];
}
function idempotencyMiddleware(options: IdempotencyOptions) {
const {
store,
headerName = 'Idempotency-Key',
methods = ['POST', 'PUT', 'PATCH'],
paths = [/.*/],
} = options;
return async (req: Request, res: Response, next: NextFunction) => {
// Only apply to specified methods
if (!methods.includes(req.method)) {
return next();
}
// Only apply to specified paths
if (!paths.some(p => p.test(req.path))) {
return next();
}
const idempotencyKey = req.headers[headerName.toLowerCase()] as string;
// No key provided - proceed without idempotency
if (!idempotencyKey) {
return next();
}
// Create a unique key combining the idempotency key with request details
const fullKey = `${req.method}:${req.path}:${idempotencyKey}`;
// Check for existing record
const existing = await store.get(fullKey);
if (existing) {
if (existing.status === 'processing') {
// Request is still being processed
return res.status(409).json({
error: 'Conflict',
message: 'A request with this idempotency key is already being processed',
});
}
if (existing.status === 'completed' && existing.response) {
// Return cached response
res.status(existing.response.statusCode);
if (existing.response.headers) {
for (const [key, value] of Object.entries(existing.response.headers)) {
res.setHeader(key, value);
}
}
res.setHeader('X-Idempotent-Replayed', 'true');
return res.json(existing.response.body);
}
}
// Try to acquire lock
const acquired = await store.acquireLock(fullKey);
if (!acquired) {
// Another request just acquired the lock
return res.status(409).json({
error: 'Conflict',
message: 'A request with this idempotency key is already being processed',
});
}
// Capture the response
const originalJson = res.json.bind(res);
let responseBody: unknown;
res.json = (body: unknown) => {
responseBody = body;
return originalJson(body);
};
// Store response after it's sent
res.on('finish', async () => {
if (res.statusCode >= 200 && res.statusCode < 500) {
// Store successful responses and client errors (but not server errors)
await store.complete(fullKey, {
statusCode: res.statusCode,
body: responseBody,
});
} else {
// Release lock for server errors (allow retry)
await store.release(fullKey);
}
});
next();
};
}
export { idempotencyMiddleware, IdempotencyOptions };
Usage
// app.ts
import express from 'express';
import { Redis } from 'ioredis';
import { IdempotencyStore } from './idempotency-store';
import { idempotencyMiddleware } from './idempotency-middleware';
const app = express();
const redis = new Redis();
const idempotencyStore = new IdempotencyStore({ redis });
// Apply to all POST/PUT/PATCH requests
app.use(idempotencyMiddleware({
store: idempotencyStore,
methods: ['POST', 'PUT', 'PATCH'],
}));
// Or apply to specific routes
app.post('/orders',
idempotencyMiddleware({
store: idempotencyStore,
paths: [/^\/orders$/],
}),
async (req, res) => {
const order = await createOrder(req.body);
res.status(201).json(order);
}
);
Python Implementation
# idempotency.py
import json
import time
from typing import Optional, Dict, Any
from dataclasses import dataclass
import redis
from functools import wraps
@dataclass
class IdempotencyRecord:
status: str # 'processing' | 'completed'
response: Optional[Dict[str, Any]] = None
created_at: float = 0
completed_at: Optional[float] = None
class IdempotencyStore:
def __init__(
self,
redis_client: redis.Redis,
key_prefix: str = "idempotency:",
lock_ttl_ms: int = 60000,
response_ttl_ms: int = 86400000,
):
self.redis = redis_client
self.key_prefix = key_prefix
self.lock_ttl = lock_ttl_ms
self.response_ttl = response_ttl_ms
def get(self, key: str) -> Optional[IdempotencyRecord]:
data = self.redis.get(self.key_prefix + key)
if not data:
return None
parsed = json.loads(data)
return IdempotencyRecord(**parsed)
def acquire_lock(self, key: str) -> bool:
record = {
"status": "processing",
"created_at": time.time(),
}
result = self.redis.set(
self.key_prefix + key,
json.dumps(record),
px=self.lock_ttl,
nx=True,
)
return result is True
def complete(self, key: str, response: Dict[str, Any]) -> None:
record = {
"status": "completed",
"response": response,
"created_at": time.time(),
"completed_at": time.time(),
}
self.redis.set(
self.key_prefix + key,
json.dumps(record),
px=self.response_ttl,
)
def release(self, key: str) -> None:
self.redis.delete(self.key_prefix + key)
FastAPI Middleware
# fastapi_idempotency.py
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
class IdempotencyMiddleware(BaseHTTPMiddleware):
def __init__(
self,
app,
store: IdempotencyStore,
header_name: str = "Idempotency-Key",
methods: list = None,
):
super().__init__(app)
self.store = store
self.header_name = header_name
self.methods
---
*Content truncated.*
When not to use it
- →For operations that are not retriable
- →When side effects are intended to occur on every request
Limitations
- →Requires unique key generation
How it compares
It provides a structured middleware and client-side pattern for handling retries compared to manual state tracking.
Compared to similar skills
idempotency side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| idempotency (this skill) | 1 | 6mo | Review | Intermediate |
| fastapi-templates | 520 | 2mo | No flags | Intermediate |
| fastapi-pro | 79 | 4mo | No flags | Advanced |
| springboot-patterns | 11 | 5mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by dadbodgeoff
View all by dadbodgeoff →You might also like
fastapi-templates
wshobson
Create production-ready FastAPI projects with async patterns, dependency injection, and comprehensive error handling. Use when building new FastAPI applications or setting up backend API projects.
fastapi-pro
sickn33
Build high-performance async APIs with FastAPI, SQLAlchemy 2.0, and Pydantic V2. Master microservices, WebSockets, and modern Python async patterns. Use PROACTIVELY for FastAPI development, async optimization, or API architecture.
springboot-patterns
affaan-m
Spring Boot 架构模式、REST API 设计、分层服务、数据访问、缓存、异步处理和日志记录。适用于 Java Spring Boot 后端工作。
backend-development
skillcreatorai
Backend API design, database architecture, microservices patterns, and test-driven development. Use for designing APIs, database schemas, or backend system architecture.
dotnet-backend-patterns
wshobson
Master C#/.NET backend development patterns for building robust APIs, MCP servers, and enterprise applications. Covers async/await, dependency injection, Entity Framework Core, Dapper, configuration, caching, and testing with xUnit. Use when developing .NET backends, reviewing C# code, or designing API architectures.
laravel-specialist
Jeffallan
Use when building Laravel 10+ applications requiring Eloquent ORM, API resources, or queue systems. Invoke for Laravel models, Livewire components, Sanctum authentication, Horizon queues.