🎯

idempotency

🎯Skill

from dadbodgeoff/drift

VibeIndex|
What it does

Implements idempotent API operations to prevent duplicate processing and safely handle retries across payment, order, and webhook systems.

πŸ“¦

Part of

dadbodgeoff/drift(69 items)

idempotency

Installation

npm installInstall npm package
npm install -g driftdetect
npm installInstall npm package
npm install -g driftdetect@latest
npm installInstall npm package
npm install -g driftdetect-mcp
Claude Desktop ConfigurationAdd this to your claude_desktop_config.json
{ "mcpServers": { "drift": { "command": "driftdetect-mcp" } } ...
πŸ“– Extracted from docs: dadbodgeoff/drift
4Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

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.

Overview

# 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

```typescript

// idempotency-store.ts

import { Redis } from 'ioredis';

interface IdempotencyRecord {

status: 'processing' | 'completed';

response?: {

statusCode: number;

body: unknown;

headers?: Record;

};

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 {

const data = await this.redis.get(this.keyPrefix + key);

return data ? JSON.parse(data) : null;

}

async acquireLock(key: string): Promise {

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 {

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 {

await this.redis.del(this.keyPrefix + key);

}

}

export { IdempotencyStore, IdempotencyRecord, IdempotencyConfig };

```

Express Middleware

```typescript

// 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

```typescript

// 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

```python

# 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

```python

# 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 = methods or ["POST", "PUT", "PATCH"]

async def dispatch(self, request: Request, call_next):

if request.method not in self.methods:

return await call_next(request)

idempotency_key = request.headers.get(self.header_name)

if not idempotency_key:

return await call_next(request)

full_key = f"{request.method}:{request.url.path}:{idempotency_key}"

# Check existing

existing = self.store.get(full_key)

if existing:

if existing.status == "processing":

raise HTTPException(

status_code=409,

detail="Request with this idempotency key is being processed",

)

if existing.status == "completed" and existing.response:

return JSONResponse(

content=existing.response["body"],

status_code=existing.response["status_code"],

headers={"X-Idempotent-Replayed": "true"},

)

# Acquire lock

if not self.store.acquire_lock(full_key):

raise HTTPException(

status_code=409,

detail="Request with this idempotency key is being processed",

)

try:

response = await call_next(request)

# Cache successful responses

if 200 <= response.status_code < 500:

body = b""

async for chunk in response.body_iterator:

body += chunk

self.store.complete(full_key, {

"status_code": response.status_code,

"body": json.loads(body),

})

return JSONResponse(

content=json.loads(body),

status_code=response.status_code,

)

else:

self.store.release(full_key)

return response

except Exception:

self.store.release(full_key)

raise

```

Decorator Pattern

```python

# idempotent_decorator.py

from functools import wraps

def idempotent(store: IdempotencyStore, key_func=None):

"""

Decorator for idempotent functions.

@idempotent(store, key_func=lambda args: args[0].order_id)

async def process_order(order: Order):

...

"""

def decorator(func):

@wraps(func)

async def wrapper(args, *kwargs):

# Generate key

if key_func:

key = key_func(args, kwargs)

else:

key = f"{func.__name__}:{hash(str(args) + str(kwargs))}"

# Check existing

existing = store.get(key)

if existing and existing.status == "completed":

return existing.response["result"]

# Acquire lock

if not store.acquire_lock(key):

raise Exception("Operation already in progress")

try:

result = await func(args, *kwargs)

store.complete(key, {"result": result})

return result

except Exception:

store.release(key)

raise

return wrapper

return decorator

# Usage

@idempotent(store, key_func=lambda args, kwargs: f"order:{kwargs.get('order_id')}")

async def process_payment(order_id: str, amount: float):

return await stripe.charges.create(amount=amount)

```

Client-Side Implementation

```typescript

// idempotent-client.ts

class IdempotentClient {

private generateKey(): string {

return crypto.randomUUID();

}

async post(url: string, data: unknown, options?: RequestInit): Promise {

const idempotencyKey = this.generateKey();

const response = await fetch(url, {

method: 'POST',

headers: {

'Content-Type': 'application/json',

'Idempotency-Key': idempotencyKey,

...options?.headers,

},

body: JSON.stringify(data),

...options,

});

if (response.status === 409) {

// Request in progress, wait and retry

await new Promise(resolve => setTimeout(resolve, 1000));

return this.postWithKey(url, data, idempotencyKey, options);

}

return response.json();

}

private async postWithKey(

url: string,

data: unknown,

idempotencyKey: string,

options?: RequestInit,

retries = 3

): Promise {

const response = await fetch(url, {

method: 'POST',

headers: {

'Content-Type': 'application/json',

'Idempotency-Key': idempotencyKey,

...options?.headers,

},

body: JSON.stringify(data),

...options,

});

if (response.status === 409 && retries > 0) {

await new Promise(resolve => setTimeout(resolve, 1000));

return this.postWithKey(url, data, idempotencyKey, options, retries - 1);

}

return response.json();

}

}

```

Best Practices

  1. Include request details in key: Method + path + idempotency key
  2. Set appropriate TTLs: Lock TTL < Response TTL
  3. Handle 409 gracefully: Client should wait and retry
  4. Don't cache server errors: Allow retry on 5xx
  5. Use UUIDs for keys: Clients should generate unique keys

Common Mistakes

  • Using sequential IDs (collisions across users)
  • Caching server errors (prevents retry)
  • Too short response TTL (client retries get new result)
  • Not including request path in key (different endpoints collide)
  • Forgetting to release lock on error

Security Considerations

  • Validate idempotency key format
  • Rate limit by idempotency key
  • Don't expose internal state in 409 responses
  • Consider per-user key namespacing

More from this repository10

🎯
feature-flags🎯Skill

Enables controlled feature rollouts, A/B testing, and selective feature access through configurable flags for gradual deployment and user targeting.

🎯
design-tokens🎯Skill

Generates a comprehensive, type-safe design token system with WCAG AA color compliance and multi-framework support for consistent visual design.

🎯
file-uploads🎯Skill

Securely validates, scans, and processes file uploads with multi-stage checks, malware detection, and race condition prevention.

🎯
ai-coaching🎯Skill

Guides users through articulating creative intent by extracting structured parameters and detecting conversation readiness.

🎯
environment-config🎯Skill

Validates and centralizes environment variables with type safety, fail-fast startup checks, and multi-environment support.

🎯
community-feed🎯Skill

Generates efficient social feed with cursor pagination, trending algorithms, and engagement tracking for infinite scroll experiences.

🎯
cloud-storage🎯Skill

Enables secure, multi-tenant cloud file storage with signed URLs, direct uploads, and visibility control for user-uploaded assets.

🎯
email-service🎯Skill

Simplifies email sending, templating, and tracking with robust SMTP integration and support for multiple email providers and transactional workflows.

🎯
error-sanitization🎯Skill

Sanitizes error messages by logging full details server-side while exposing only generic, safe messages to prevent sensitive information leakage.

🎯
batch-processing🎯Skill

Optimizes database operations by collecting and batching independent records, improving throughput by 30-40% with built-in fallback processing.