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.zip

Installs 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.
191 chars✓ has a “when” trigger
Intermediate

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

You give it
API request with an idempotency key
You get back
Cached response or processed result

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.

SkillInstallsUpdatedSafetyDifficulty
idempotency (this skill)16moReviewIntermediate
fastapi-templates5202moNo flagsIntermediate
fastapi-pro794moNo flagsAdvanced
springboot-patterns115moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry