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

推荐订阅源

freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
G
Google Developers Blog
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
S
SegmentFault 最新的问题
宝玉的分享
宝玉的分享
博客园 - Franky
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
月光博客
月光博客
博客园 - 聂微东
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
小众软件
小众软件
Microsoft Security Blog
Microsoft Security Blog
Last Week in AI
Last Week in AI
Vercel News
Vercel News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
爱范儿
爱范儿
J
Java Code Geeks
博客园 - 叶小钗
Engineering at Meta
Engineering at Meta
阮一峰的网络日志
阮一峰的网络日志

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 Simple Trick That Prevents Duplicat...
Mean · 2026-05-22 · via DEV Community

Idempotency Keys: The Simple Trick That Prevents Duplicate Payments and Ghost Orders

You've seen it happen. A user clicks "Place Order," the network hiccups, the request times out, and now they've been charged twice. Or the charge went through but the order never appeared. Neither outcome is acceptable, and both are preventable — with idempotency keys.

What Is an Idempotency Key?

An idempotency key is a unique identifier you attach to a request so that the server can safely process it exactly once, no matter how many times the client retries. If the server receives the same key again, it returns the original response instead of executing the operation a second time.

The concept is borrowed from mathematics: an operation is idempotent if applying it multiple times produces the same result as applying it once. DELETE /users/42 is naturally idempotent — deleting an already-deleted user changes nothing. POST /orders is not — sending it twice creates two orders.

Idempotency keys make non-idempotent operations safe to retry.

How It Works in Practice

The client generates a unique key (typically a UUID v4) before sending the request. It attaches the key as a header, usually Idempotency-Key. The server stores the key and the response in a short-lived cache (24 hours is common). On retry, the server finds the key, skips execution, and returns the stored response.

Here's a payment request using this pattern in Python:

import uuid
import httpx

def charge_customer(customer_id: str, amount_cents: int) -> dict:
    idempotency_key = str(uuid.uuid4())  # generate once, store for retries

    headers = {
        "Authorization": "Bearer sk_live_...",
        "Idempotency-Key": idempotency_key,
        "Content-Type": "application/json",
    }

    payload = {
        "customer": customer_id,
        "amount": amount_cents,
        "currency": "usd",
    }

    for attempt in range(3):
        try:
            response = httpx.post(
                "https://api.stripe.com/v1/charges",
                headers=headers,   # same key on every retry
                json=payload,
                timeout=10,
            )
            response.raise_for_status()
            return response.json()
        except (httpx.TimeoutException, httpx.ConnectError):
            if attempt == 2:
                raise
            continue

Enter fullscreen mode Exit fullscreen mode

The critical detail: the idempotency_key is generated once before the loop, not inside it. Every retry sends the identical key, so the server knows they're all the same request.

Implementing the Server Side

If you're building an API that accepts payments, bookings, or any state-changing operation, you need to store and check idempotency keys yourself.

import { randomUUID } from "crypto";
import { redis } from "./redis";

async function handleCharge(req: Request): Promise<Response> {
  const key = req.headers.get("Idempotency-Key");

  if (!key) {
    return Response.json({ error: "Idempotency-Key header required" }, { status: 400 });
  }

  // Check for a cached response
  const cached = await redis.get(`idem:${key}`);
  if (cached) {
    return Response.json(JSON.parse(cached), { status: 200 });
  }

  // Execute the operation
  const result = await processCharge(req);

  // Store result for 24 hours
  await redis.set(`idem:${key}`, JSON.stringify(result), "EX", 86400);

  return Response.json(result, { status: 201 });
}

Enter fullscreen mode Exit fullscreen mode

A few implementation notes worth keeping in mind:

Scope keys to the user. Store the key as user:{userId}:idem:{key} to prevent one user from accidentally (or maliciously) colliding with another's key.

Don't cache errors. If the operation fails with a 500, don't store the result — let the client retry with the same key and give the operation another chance. Do cache 4xx errors, since a bad request won't succeed on retry.

Return the same status code. If the original response was 201 Created, the cached response should also be 201, not 200. Clients sometimes branch on status codes.

When to Require Idempotency Keys

Not every endpoint needs them. GET requests are already safe to repeat. Natural idempotent operations (DELETE, PUT with full resource replacement) don't need extra help. But you should require idempotency keys for:

  • Payment and refund creation
  • Order placement
  • Email or SMS sends
  • Any operation that creates a resource or triggers a side effect

Stripe, Braintree, and Adyen all require idempotency keys on charge endpoints for exactly this reason. If you're building a public API, following their lead protects your users and your data integrity.

Testing Idempotency

Before shipping, verify the behaviour explicitly in your test suite:

def test_duplicate_charge_is_ignored():
    key = str(uuid.uuid4())
    first  = client.post("/charges", headers={"Idempotency-Key": key}, json=payload)
    second = client.post("/charges", headers={"Idempotency-Key": key}, json=payload)

    assert first.status_code == 201
    assert second.status_code == 201
    assert first.json()["id"] == second.json()["id"]  # same charge, not a new one
    assert db.count_charges() == 1                    # only one record created

Enter fullscreen mode Exit fullscreen mode


Idempotency keys are one of those small API design choices with an outsized reliability impact. They're cheap to implement and expensive to skip — your support team (and your users) will thank you.

If you want to test idempotency key behaviour against your API without writing scaffolding from scratch, APIKumo lets you set custom headers like Idempotency-Key per-request or as collection-level defaults, so you can replay the same request with identical headers and verify your server handles duplicates correctly. It's a quick sanity check before you ship.