惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
美团技术团队
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
V
Visual Studio Blog
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
云风的 BLOG
云风的 BLOG
有赞技术团队
有赞技术团队
T
The Blog of Author Tim Ferriss
WordPress大学
WordPress大学
月光博客
月光博客
宝玉的分享
宝玉的分享
小众软件
小众软件
MongoDB | Blog
MongoDB | Blog
Apple Machine Learning Research
Apple Machine Learning Research
A
About on SuperTechFans
J
Java Code Geeks
博客园_首页
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
Vercel News
Vercel News
博客园 - 聂微东

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Idempotency Keys: The API Pattern That Saves You From Dup...
Mean · 2026-05-26 · via DEV Community

Idempotency Keys: The API Pattern That Saves You From Duplicate Payments and Phantom Records

Your user clicks "Pay" and nothing happens. So they click again. Your server processes both requests — and charges the card twice.

This is one of the most damaging bugs you can ship, and it's entirely preventable. The solution has been hiding in plain sight in Stripe's API since 2013: idempotency keys.

What Is an Idempotency Key?

An idempotency key is a unique token the client sends with a mutating request (POST, PATCH, DELETE). The server uses it to guarantee that no matter how many times you send the same request — due to retries, network failures, or impatient button-clicks — the operation only executes once.

The word comes from mathematics: a function f is idempotent if f(f(x)) = f(x). Applying it twice is the same as applying it once.

Stripe implements it like this:

POST /v1/charges
Authorization: Bearer sk_live_...
Idempotency-Key: charge_user_42_order_891
Content-Type: application/json

{
  "amount": 2000,
  "currency": "usd",
  "source": "tok_visa"
}

If the client sends this request again with the same Idempotency-Key, Stripe returns the cached response from the first execution — no second charge.

Implementing Idempotency Keys in Your Own API

Here's a minimal implementation in Node.js with Express and Redis:

const express = require('express');
const redis = require('redis');
const { v4: uuidv4 } = require('uuid');

const app = express();
const cache = redis.createClient();
const IDEMPOTENCY_TTL = 86400; // 24 hours in seconds

app.use(express.json());

async function idempotencyMiddleware(req, res, next) {
  const key = req.headers['idempotency-key'];

  // Only applies to mutating methods
  if (!['POST', 'PATCH', 'DELETE'].includes(req.method) || !key) {
    return next();
  }

  const cacheKey = `idempotency:${key}`;
  const cached = await cache.get(cacheKey);

  if (cached) {
    const { statusCode, body } = JSON.parse(cached);
    return res.status(statusCode).json(body);
  }

  // Intercept the response to cache it
  const originalJson = res.json.bind(res);
  res.json = async (body) => {
    if (res.statusCode < 500) {
      await cache.setEx(
        cacheKey,
        IDEMPOTENCY_TTL,
        JSON.stringify({ statusCode: res.statusCode, body })
      );
    }
    return originalJson(body);
  };

  next();
}

app.use(idempotencyMiddleware);

app.post('/charges', async (req, res) => {
  // Your actual charge logic here
  const charge = await processCharge(req.body);
  res.status(201).json({ id: charge.id, status: 'succeeded' });
});

Key design decisions baked in:

  • Only mutating methods get the idempotency treatment. GET requests are already safe to retry.
  • 500 errors are never cached. A server crash shouldn't lock the client into a permanent error.
  • 24-hour TTL. Long enough to cover any reasonable retry window, short enough to not bloat your cache forever.

Generating Idempotency Keys on the Client

The key should be unique per logical operation, not per HTTP request. Here's a pattern that works:

import uuid
import hashlib

def make_idempotency_key(user_id: str, operation: str, resource_id: str) -> str:
    """
    Deterministic key: same inputs always produce the same key.
    Safe to call multiple times before sending.
    """
    raw = f"{user_id}:{operation}:{resource_id}"
    return hashlib.sha256(raw.encode()).hexdigest()[:32]

# Example usage
key = make_idempotency_key(
    user_id="user_42",
    operation="charge",
    resource_id="order_891"
)
# → always produces the same key for this user + order combination

import httpx

response = httpx.post(
    "https://api.example.com/charges",
    headers={"Idempotency-Key": key},
    json={"amount": 2000, "currency": "usd"}
)

Using a deterministic hash instead of a random UUID means the client can reconstruct the key after a crash, without needing to store it separately.

What About Conflicts?

What if the same idempotency key arrives with a different request body? This indicates a client bug — not a legitimate retry. Return a 422 Unprocessable Entity:

async function idempotencyMiddleware(req, res, next) {
  const key = req.headers['idempotency-key'];
  if (!key) return next();

  const cacheKey = `idempotency:${key}`;
  const cached = await cache.get(cacheKey);

  if (cached) {
    const stored = JSON.parse(cached);
    const bodyHash = hashBody(req.body);

    if (stored.bodyHash !== bodyHash) {
      return res.status(422).json({
        error: 'idempotency_key_reuse',
        message: 'This idempotency key was used with a different request body.'
      });
    }

    return res.status(stored.statusCode).json(stored.body);
  }

  // Store body hash alongside the response
  req.idempotencyBodyHash = hashBody(req.body);
  next();
}

function hashBody(body) {
  return require('crypto')
    .createHash('sha256')
    .update(JSON.stringify(body))
    .digest('hex');
}

The Header Name Convention

There's no RFC standard here yet, but the de facto conventions are:

Header Used by
Idempotency-Key Stripe, Adyen, many fintech APIs
X-Idempotency-Key Some older APIs
X-Request-ID Often used for tracing, sometimes idempotency

Stick with Idempotency-Key for new APIs — it's become the community standard.

Testing Idempotency in Your API Client

Before shipping idempotency support, you want to verify that duplicate requests actually return the same cached response. This is where having a good API testing environment matters enormously. With APIKumo, you can create a collection with pre-request scripts that automatically generate and re-use idempotency keys across retried requests — making it straightforward to simulate retry scenarios and confirm your server behaves correctly, without writing a separate test harness.

Wrapping Up

Idempotency keys are a small header with enormous consequences. They're the difference between a payment API users trust and one that occasionally charges customers twice. The implementation is under 50 lines of middleware, the client-side change is a single header, and the protection they provide is permanent.

If you're building any API that processes money, creates records, or sends messages — idempotency keys should be non-negotiable.