Skip to main content

Command Palette

Search for a command to run...

Idempotency vs Nonce: Two Weapons Against Duplicate Requests

Updated
5 min read
Idempotency vs Nonce: Two Weapons Against Duplicate Requests

Every backend developer eventually faces this scenario: a user clicks "Pay" twice, a network timeout causes a client to retry, or a message queue delivers the same event more than once. These situations can create duplicate orders, double charges, or corrupted data.

Two battle-tested strategies exist to defend against this:

Idempotency and Nonce. They look similar on the surface, but they serve different purposes. Let's break them down.

The Problem: Why Do Duplicates Happen?

Before diving into solutions, let's understand the enemy.

Client  ──── POST /pay ────►  Server
Client  ◄──── timeout ──────  Server (processing...)
Client  ──── POST /pay ────►  Server  ← 💣 DUPLICATE!

The client never got a response, so it retried — but the first request was already processed. Now you have two charges.

This happens in:

  • Mobile apps on unstable connections

  • Message queues (at-least-once delivery)

  • Retry logic in service-to-service communication

  • Frontend double-clicks

Solution 1: Idempotency

What Is It?

An operation is idempotent if calling it once or calling it many times produces the same result.

f(f(x)) = f(x)

Think of a light switch that only turns on: pressing it once or ten times — the light is on. Same result.

In HTTP terms: GET, PUT, and DELETE are naturally idempotent. POST is not — that's where you need to enforce it yourself.

How It Works

The client generates a unique Idempotency Key and sends it with every request. The server stores the result of the first execution. On any subsequent request with the same key, the server returns the cached result without re-executing the operation.

Request 1: POST /pay  {idempotency-key: "abc-123"}  → Processed ✅  (result saved)
Request 2: POST /pay  {idempotency-key: "abc-123"}  → Returned from cache ✅  (not re-executed)
Request 3: POST /pay  {idempotency-key: "abc-123"}  → Returned from cache ✅  (not re-executed)

Important Notes

  • The key should be generated by the client (usually a UUID v4).

  • Store idempotency records in a persistent store (DB or Redis), not in memory.

  • Set a TTL on records (e.g., 24 hours or 7 days) — you don't need them forever.

  • Handle concurrent requests with the same key using a database unique constraint or distributed lock.

Solution 2: Nonce

What Is It?

Nonce stands for "Number used Once". It's a unique token that is valid for a single use only. Once consumed, it's permanently invalidated — even if the operation behind it failed.

The key difference from idempotency: a nonce doesn't cache results. It just asks: "Has this token been used before?"

How It Works

Request 1: POST /transfer  {nonce: "xyz-789"}  → Valid, consumed ✅
Request 2: POST /transfer  {nonce: "xyz-789"}  → REJECTED ❌ (already used)

Even if request 1 failed for some business reason, request 2 with the same nonce is still rejected. The client must generate a new nonce for a new attempt.

Important Notes

  • You can use Redis SETNX (SET if Not eXists) for atomic check-and-consume. Never do a separate GET then SET — that's a race condition.

  • Set a reasonable TTL based on your use case (5–15 minutes for form submissions, longer for payment flows).

  • The client must generate a new nonce for every new attempt. Nonces are not reusable.

  • Nonces work great for one-time form submissions, OTP verification, and CSRF protection.

Side-by-Side Comparison

Idempotency Nonce
Core question "Has this operation been done before?" "Has this token been used before?"
On duplicate request Returns cached result Rejects with error
Client retry behavior Safe to retry with same key Must generate a new nonce
Stores Full request result Just the token
Best for Payment APIs, order creation, idempotent POST endpoints Form submissions, OTPs, CSRF, single-use links
Response on duplicate 200 OK (cached) 409 Conflict
Key generated by Client Client (or server pre-issues it)
TTL Hours to days Minutes

When to Use Which?

Is the client allowed to retry with the same intent?
    │
    ├── YES → Use Idempotency
    │         (same operation, same result expected)
    │         Example: "Charge $100 for order #456"
    │
    └── NO  → Use Nonce
              (one shot only, new attempt = new token)
              Example: "Submit this form", "Verify this OTP"

Real-world rule of thumb:

  • Payment APIs → Idempotency (Stripe, PayPal both use this)

  • Authentication flows, form submissions → Nonce

  • CSRF protection → Nonce

  • Webhook delivery → Idempotency

Combining Both

In high-security systems, you can use both together. This gives you the best of both worlds: replay protection in the short term (nonce) and safe retries over time (idempotency).

Summary

Concept One-liner
Idempotency Same key → same result, always safe to retry
Nonce Single-use token → rejected on second use

Both patterns are essential tools in any production backend. Idempotency is your friend when you need fault-tolerant retry logic. Nonce is your guard against replay attacks and accidental double submissions.

Once you start thinking in these terms, you'll spot the need for them everywhere.