CMD Guide
HomeSystem DesignScalable Systems (Advanced Topics)

What Is A Replay Attack, And How Is It Different from Idempotency Issues

A replay attack works because a captured message already carries every proof of authenticity the server checks — a valid signature, a live session token, an authenticated envelope — so re-sending the identical bytes re-passes every one of those checks, and the server, having no way to tell "this is the second time I've seen this" from "this is a new request," performs the action again.

An idempotency issue produces the same visible symptom — one intent, two executions — but the cause is honest: a slow response, a double-click, or a client timeout-and-retry resends a request nobody meant to duplicate. The distinction is not the symptom; it is intent plus what defense stops it. Replays are defeated by freshness (each request may be accepted at most once, within a short window); accidental duplicates are defeated by execution de-duplication (remember the first result and return it again). This page separates the two mechanisms cleanly, traces both with real values, and shows when each defense — and each alternative — is the right call.

Two mechanisms, side by side

Malicious replay. An attacker captures a legitimate message and re-sends it unchanged to repeat its effect. A common misconception is that this requires a full man-in-the-middle position. It does not: replay needs only passive capture plus the ability to send to the server — sniffing an unencrypted or metadata-leaking channel, reading a logged request, or grabbing a token from a browser is enough. A true MITM (who can also modify traffic in flight) is a strictly stronger, and rarer, attacker. Replay is dangerous precisely because the weaker, passive attacker can pull it off. The defense is freshness: bind each request to a one-time nonce and a timestamp, cover both with a signature, and reject anything stale or previously seen.

Accidental idempotency issue. No attacker. A retry, a refresh, or an at-least-once message queue delivers the same intent twice. The defense is an idempotency key: the client attaches a unique id per intent, the server stores the result of the first execution against that key, and any later request with the same key gets the stored result replayed back instead of a second side effect.

They overlap at one point: an attacker who replays a request against a non-idempotent endpoint gets a real duplicate side effect — the accidental-duplication bug becomes an exploit. This is why idempotency keys blunt replays too, but note the asymmetry: an idempotency key stops a duplicate side effect; it does not stop an attacker from replaying to read back a stored response, nor does it authenticate the caller. Freshness + signatures do that. The two defenses are complementary, not substitutes.

diagram
diagram

Worked trace: the freshness gate

A payment client signs each request over its timestamp, a random nonce, and the body. The server keeps a nonce cache whose TTL equals its acceptance window (300 s). Watch what happens to Alice's real request and to Eve's two replay attempts.

Eventserver clockts in msgnoncesig valid?|now−ts|≤300?nonce seen?Result
Alice's real request17199000021719900000a3f9…yes2 s → yesno → cache it200 OK, charge $100
Eve replays after 60 s17199000621719900000a3f9…yes62 s → yesyes409 rejected (nonce reuse)
Eve replays after 6 min17199003801719900000a3f9…yes380 s → no(evicted anyway)401 rejected (stale)

The elegant part: because the nonce cache TTL equals the freshness window, a fresh replay is caught by the nonce check (step 3) and a slow replay is caught by the timestamp check (step 2). The server never has to remember nonces forever — the timestamp bounds how long any single nonce could still matter. Signature alone would not stop the 60 s replay: the bytes are byte-identical, so the signature is still valid. Freshness, not the signature, is what defeats replay.

The two defenses in code

Freshness gate (stops malicious replay) — signature is necessary but not sufficient; the nonce + timestamp are what add "at most once":

def handle(req):
    ts    = req.headers["X-Timestamp"]   # unix seconds
    nonce = req.headers["X-Nonce"]       # 128-bit random, per request
    sig   = req.headers["X-Signature"]

    # 1. Signature must cover ts + nonce + body (else attacker edits ts freely)
    expected = hmac_sha256(secret, ts + nonce + req.body)
    if not constant_time_eq(sig, expected):
        return 401  # forged or tampered

    # 2. Freshness window: an old capture is useless
    if abs(now() - int(ts)) > 300:
        return 401  # stale (or clock skew)

    # 3. One-time use: atomic "add if absent" over a TTL = the window
    if not nonce_cache.add_if_absent(nonce, ttl=300):
        return 409  # replay: this nonce was already accepted

    return do_work(req)

Idempotency store (stops accidental duplicates) — the naive version has a race; the correct version reserves the key atomically before the side effect:

# WRONG: check-then-act. Two concurrent double-clicks both see no row,
# both fall through, and charge_card runs twice.
def create_payment_naive(req):
    key = req.headers["Idempotency-Key"]
    if store.get(key):                 # both requests: None
        return store.get(key).response
    resp = charge_card(req.body)       # runs TWICE under a race
    store.put(key, response=resp)
    return resp

# CORRECT: atomic insert reserves the key first (INSERT ... ON CONFLICT).
def create_payment(req):
    key = req.headers["Idempotency-Key"]  # client-generated UUID per intent
    if not store.insert_if_absent(key, state="in_flight"):
        row = store.get(key)
        if row.state == "completed":
            return row.response          # replay the stored result, no re-charge
        return 409                       # concurrent duplicate still running
    resp = charge_card(req.body)         # the real, non-idempotent effect
    store.update(key, state="completed", response=resp)
    return resp

Why the naive version is wrong: get then put is a read-modify-write with a gap. Two duplicate requests arriving within that gap both read "absent" and both proceed to charge_card — the exact double-charge the key was supposed to prevent. The fix is a single atomic operation (insert_if_absent / INSERT … ON CONFLICT DO NOTHING / SETNX) that lets exactly one request win the key before any side effect runs. This is Stripe's model: clients send an Idempotency-Key, the first request's result is stored and replayed for 24 h.

Pitfalls

When to use which — and the trade-offs

These are three defenses against "the same request twice." Choosing among them is a judgment about who is repeating the request and what you must prevent.

Nonce cache (reject-if-seen)

Choose it when the threat is a malicious replay and you need a hard "exactly once" over a short window — payment authorizations, auth handshakes, signed webhooks. Gain: true one-time acceptance. Cost: shared, low-latency state (Redis) on the hot path, and its memory grows with request rate × TTL; every request pays a network round-trip to the cache.

Timestamp window alone (no nonce)

Choose it when you want cheap, stateless replay resistance and can tolerate replays within the window. Gain: zero shared state — each node checks the clock independently. Cost: it only shrinks the attack window, it doesn't close it; anything inside the window replays freely. Prefer the nonce cache when a single duplicate is unacceptable (money); prefer timestamp-only when the operation is naturally idempotent and you just want to bound staleness.

Idempotency key (store-and-replay)

Choose it when the repeats are honest — retries, at-least-once queues, double-clicks — and you need the duplicate to be a no-op that returns the original result. Gain: safe retries, better UX, and it happens to blunt replays too. Cost: durable per-key storage with a retention policy, careful state machine (in-flight vs completed vs failed), and it authenticates nothing.

The senior call: for a money-moving endpoint on an open network, you layer them — TLS + a signature (authenticate), a nonce + timestamp (stop replay), and an idempotency key (make honest retries safe). They defend different failure modes; picking one and calling it done is the mistake. If you must pick one for an internal, already-authenticated service whose real problem is retry storms, the idempotency key earns its keep; if the problem is an untrusted network and forged replays, start with nonce + timestamp + signature.

Takeaways


Sources: RFC 2617 / RFC 7616 (HTTP Digest nonces and replay), OWASP guidance on replay prevention and cryptographic freshness, the Stripe API reference on idempotent requests (Idempotency-Key semantics and 24-hour result retention), and Kleppmann, Designing Data-Intensive Applications (at-least-once delivery, deduplication, and exactly-once processing). Re-authored and deepened for this guide.

🤖 Don't fully get this? Learn it with Claude

Stuck on What Is A Replay Attack, And How Is It Different from Idempotency Issues? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **What Is A Replay Attack, And How Is It Different from Idempotency Issues** (System Design) and want to truly understand it. Explain What Is A Replay Attack, And How Is It Different from Idempotency Issues from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **What Is A Replay Attack, And How Is It Different from Idempotency Issues** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **What Is A Replay Attack, And How Is It Different from Idempotency Issues** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **What Is A Replay Attack, And How Is It Different from Idempotency Issues** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes