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

推荐订阅源

人人都是产品经理
人人都是产品经理
Stack Overflow Blog
Stack Overflow Blog
S
SegmentFault 最新的问题
博客园 - 司徒正美
aimingoo的专栏
aimingoo的专栏
U
Unit 42
GbyAI
GbyAI
B
Blog RSS Feed
博客园 - Franky
L
LangChain Blog
Hugging Face - Blog
Hugging Face - Blog
美团技术团队
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
云风的 BLOG
云风的 BLOG
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 三生石上(FineUI控件)
Microsoft Azure Blog
Microsoft Azure Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
G
Google Developers Blog
Last Week in AI
Last Week in AI
阮一峰的网络日志
阮一峰的网络日志
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research

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 Safety Net You Probably Aren't ...
Mean · 2026-05-23 · via DEV Community

Idempotency Keys: The API Safety Net You Probably Aren't Using

Network failures happen at the worst possible times. A user clicks "Pay Now," your request hits the payment API, the connection drops before the response arrives — and now you don't know if the charge went through. Do you retry and risk a double charge? Do you let it fail and frustrate the user?

Idempotency keys solve this problem cleanly. They're one of those API patterns that seems like a small detail until the moment you desperately need it, and then they feel like magic.

What Is an Idempotency Key?

An idempotency key is a unique value you generate on the client side and attach to a request. The server uses it to de-duplicate: if it sees the same key twice, it returns the cached result of the first request rather than executing the operation again.

The practical effect is that retrying a failed request is always safe. The operation runs once — exactly once — no matter how many times you send it.

The Classic Problem Without Idempotency

import requests

def charge_customer(customer_id: str, amount_cents: int) -> dict:
    response = requests.post(
        "https://api.payments.example/charges",
        json={"customer_id": customer_id, "amount": amount_cents},
        headers={"Authorization": "Bearer sk_live_..."}
    )
    response.raise_for_status()
    return response.json()

# If this times out after the server processed it, calling it again = double charge
charge_customer("cus_abc123", 4999)

Enter fullscreen mode Exit fullscreen mode

If raise_for_status() throws a ConnectionError or a timeout, you have no idea whether the charge happened. You're stuck.

Adding Idempotency Keys

Most APIs that care about this (Stripe, Brex, Adyen) accept idempotency keys via a header:

import uuid
import requests
from typing import Optional

def charge_customer_safely(
    customer_id: str,
    amount_cents: int,
    idempotency_key: Optional[str] = None
) -> dict:
    # Generate a stable key for this specific operation
    key = idempotency_key or str(uuid.uuid4())

    response = requests.post(
        "https://api.payments.example/charges",
        json={"customer_id": customer_id, "amount": amount_cents},
        headers={
            "Authorization": "Bearer sk_live_...",
            "Idempotency-Key": key,
        }
    )
    response.raise_for_status()
    return response.json()

Enter fullscreen mode Exit fullscreen mode

Now you can retry freely:

import time

key = str(uuid.uuid4())  # Generate ONCE, reuse on retries
max_retries = 3

for attempt in range(max_retries):
    try:
        result = charge_customer_safely("cus_abc123", 4999, idempotency_key=key)
        print(f"Charged successfully: {result['id']}")
        break
    except requests.exceptions.Timeout:
        if attempt == max_retries - 1:
            raise
        time.sleep(2 ** attempt)  # Exponential backoff

Enter fullscreen mode Exit fullscreen mode

The key insight: the key is generated before the first attempt, not per-attempt. Reusing the same key across retries is what makes this safe.

Implementing Idempotency on Your Own API

If you're building an API, adding idempotency support is worth the effort. Here's a minimal implementation in Node.js with Express and Redis:

const express = require('express');
const redis = require('redis');

const app = express();
const client = redis.createClient();
app.use(express.json());

async function idempotencyMiddleware(req, res, next) {
  const key = req.headers['idempotency-key'];
  if (!key) return next(); // Key is optional — skip if absent

  const cached = await client.get(`idem:${key}`);
  if (cached) {
    const { status, body } = JSON.parse(cached);
    return res.status(status).json(body);
  }

  // Intercept the response to cache it
  const originalJson = res.json.bind(res);
  res.json = async (body) => {
    await client.setEx(
      `idem:${key}`,
      86400, // Cache for 24 hours
      JSON.stringify({ status: res.statusCode, body })
    );
    return originalJson(body);
  };

  next();
}

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

Enter fullscreen mode Exit fullscreen mode

A few rules to enforce:

  • Scope keys to the authenticated user. User A's key should never collide with User B's.
  • Return 422 if the same key is reused with different request bodies. Mismatched payloads suggest a bug in the client.
  • Set a reasonable TTL. 24 hours is standard; some APIs go up to 7 days.

What Makes a Good Idempotency Key?

Keys should be:

  • Unique per logical operation — not per HTTP request
  • Cryptographically random — UUID v4 is the standard choice
  • Stable under retries — generate once, persist it until the operation succeeds

A common mistake is generating a new UUID on every retry. That completely defeats the purpose.

// ❌ Wrong — new key per attempt
for (let i = 0; i < 3; i++) {
  await fetch('/charges', {
    headers: { 'Idempotency-Key': crypto.randomUUID() } // New key each time!
  });
}

// ✅ Correct — stable key across all retries
const key = crypto.randomUUID(); // Generated once
for (let i = 0; i < 3; i++) {
  await fetch('/charges', {
    headers: { 'Idempotency-Key': key } // Reused across retries
  });
}

Enter fullscreen mode Exit fullscreen mode

Which Endpoints Need Idempotency?

Not all operations need this treatment. A good rule of thumb:

  • Yes: POST /charges, POST /orders, POST /transfers — anything that moves money or creates unique records
  • Yes: Any webhook delivery retry mechanism
  • No: GET and DELETE — these are already naturally idempotent
  • Judgment call: PUT — it's idempotent by definition in REST, but explicit keys don't hurt for extra safety

Testing Idempotent Endpoints

Verify your implementation handles three cases:

KEY="test-key-$(date +%s)"

# First request — should process normally
curl -X POST https://api.example.com/charges \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 4999}' \
  | jq '.id'  # Note the returned ID

# Second request with same key — should return identical response
curl -X POST https://api.example.com/charges \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 4999}' \
  | jq '.id'  # Should be the same ID

# Same key, different body — should return 422
curl -X POST https://api.example.com/charges \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 9999}' \
  | jq '.error'

Enter fullscreen mode Exit fullscreen mode

If your API passes all three, your idempotency implementation is solid.


Idempotency keys are one of those features that's invisible when it works and catastrophic when it's missing. If you're working with payment flows, order creation, or any operation your users would be upset to see run twice, add idempotency support now — before the production incident.

If you want to test how your idempotent endpoints behave end-to-end, APIKumo makes it straightforward to craft requests with custom headers, save idempotency keys as environment variables, and replay the same request multiple times to verify your de-duplication logic holds up. It's the kind of sanity check that's easy to skip and painful to skip.