Every layer that retries multiplies the layer beneath it. Enter what each one is configured to do, not what anyone believes it does; these values are usually in three different repositories owned by three teams.
These are the retries that arrive days later, and they are where the duplicate charges live. A window sized for the automatic path has long since expired by the time somebody reruns Monday's failed batch.
| Path | Delay before a duplicate can arrive | What it is | Your window |
|---|---|---|---|
| Automatic retries | 2.1 h | every layer exhausting its budget | covered |
| Dead-letter replay | 26 h | after the automatic path gives up | not covered |
| Operator rerun | 3.0 days | somebody reruns the failed batch | not covered |
Attempts for one intent, by retry layers
What a single business action can become once every layer has taken its turn. Each row is how many layers retry; each column is how many attempts each layer makes. Every one of these systems is correctly configured and every individual limit is respected.
| Retrying layers | 2 attempts each | 3 attempts each | 4 attempts each | 5 attempts each |
|---|---|---|---|---|
| 1 layer | 2 | 3 | 4 | 5 |
| 2 layers | 4 | 9 | 16 | 25 |
| 3 layers | 8 | 27 | 64 | 125 |
| 4 layers | 16 | 81 | 256 | 625 |
Amber is 16 attempts or more, red is 64 or more. A typical stack has three retrying layers before anyone counts the queue consumer, the API gateway or the client's own retry, so the realistic row is usually lower than the real one.
A worked example
A payments service with three retrying layers, each configured to three attempts: the HTTP client at two seconds, the calling service at one minute, the job scheduler at one hour. Deduplication records are kept for 24 hours, which comfortably covers the automatic path.
The automatic path exhausts in just over two hours, so a 24-hour window looks generous, and for two years it is. That is 27 attempts for one intent, which nobody has ever written down, but every one of them carries the same key and the window holds.
Then a Friday evening deploy fails a batch. Nobody is on call for a non-urgent job, so it is rerun at 9am Monday: 72 hours later. The keys expired 48 hours before that. Every payment in the batch is processed as new, correctly, by a system with a working idempotency implementation.
The required window is 72 hours times a safety factor, so six days rather than one. The fix is a configuration change and a slightly larger table. The cost of not making it is a duplicate charge for every item in a failed batch, discovered by customers, in a system everybody believed was protected.
The arithmetic, so you can check it
Attempts are the product across layers: attempts per layer, multiplied, because each outer retry replays the entire budget beneath it. The automatic delay follows the same nesting, computed from the inside out: a layer contributes backoff x (attempts - 1) for its own retries, plus the whole inner path once per attempt.
The window is then the longest path of any kind, automatic or human, multiplied by a safety factor. Human paths are not modelled as retries because they are not: they are a delay with a person at the end of it, and the only number that matters is how long it can be.
This computes what your configuration permits, not what your traffic does. A window sized from it is correct and possibly generous. What it cannot tell you is whether deduplication sits at the boundary that performs the irreversible effect: a key kept for a month at the API gateway protects nothing if the ledger write happens twice underneath it. Check where the effect occurs before you trust the number.
The four properties an idempotency key needs are in idempotency for money movement, and what it does not solve in durable execution, idempotency, or both.
Questions
Longer than the longest retry path in the entire system, including the ones nobody counts: dead-letter replays, operator reruns of a failed batch, and restores from backup that replay a queue. Those arrive days after a window sized for automatic retries has expired, and the system then processes the operation as new, correctly by its own rules, and the effect happens twice.
Because each outer layer re-runs the entire budget beneath it. An HTTP client retrying three times inside a service retrying three times inside a scheduler retrying three times is twenty-seven attempts for one business intent, and every individual layer is correctly inside its own limit. Nothing is violated and nothing alerts.
Almost never the automatic one. A Friday evening failure rerun on Monday morning is 72 hours, and a restore from backup can replay a queue a week later. Those two dominate every automatic path, and they are the ones that never appear in a retry configuration because they are a person rather than a setting.
At the last boundary that can still cause the effect twice. If the effect is a ledger write, the ledger enforces uniqueness on the key. If it is a call to an external processor, the key is passed through rather than consumed by your service. Deduplicating at the API boundary protects against two API calls, not against one call whose downstream effect is applied twice.
Rarely, and the comparison is the point. A key and a stored result per operation for a week is a small table by any measure, set against a duplicate payment and the investigation that follows it. If the required window feels uncomfortably long, that discomfort is information about your retry architecture rather than about your storage bill.