Duplicate Suppression Is End-to-End — One Retry, Three Hops
the same transfer is retried at every layer — only an ID carried from client to database stops it twice
time
not to scale
ZONE 1 · hop 1: TCP duplicate suppression
ZONE 2 · hop 2: the timeout gap
ZONE 3 · the database: constraint does the job
one TCP connection: app → DB
app
DB
SYN · original
retry
✗
dropped
TCP suppresses duplicates
within ONE connection
conn breaks → new conn: TCP cannot help
browser
server
POST /transfer
req=0286…
COMMIT sent,
reply lost — timed out?
retry: same POST
req=0286…
retry = separate request, separate transaction
app
DB
INSERT req=…
✓
same req ID
✗
UNIQUE(request_id)
rejects the dup — 2nd tx aborts
moved $11 once, not $22
Example 13-1 — NOT idempotent: a retry moves $22, not $11
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance + 11.00
UPDATE accounts SET balance = balance - 11.00
COMMIT; -- to 1234 / from 4321
retried after timeout ⇒ $22 moved instead of $11
Example 13-2 — idempotent: UNIQUE(request_id), INSERT-first
ALTER TABLE requests ADD UNIQUE (request_id);
BEGIN TRANSACTION;
INSERT INTO requests VALUES('0286FDB8…', 4321, 1234, 11.00);
UPDATEs + COMMIT; -- 2nd INSERT fails, tx aborts
duplicate rejected ⇒ $11 moved once, not $22
TCP dedup (one connection) ⊊ DB transactions (one hop) ⊊ request ID, client → database (end to end)
each inner layer narrows duplicates — but only the end-to-end ID solves it
request
retry / 2nd attempt
✗
rejected
reply lost / timeout
sources: Example 13-1 ($11/$22, accounts 1234/4321) · Example 13-2 (request_id 0286FDB8…, INSERT-first transaction)
Saltzer–Reed–Clark (1984): duplicate suppression can only be solved end to end — lower layers only reduce the odds