Tool

Do your nested timeouts actually work?

Timeouts have to decrease inward and retry counts multiply, so a stack where every layer looks reasonable on its own can be one where almost none of the configured attempts can finish. Those attempts still hit the dependency.

The stack, outermost first

List the layers in the order a request passes through them, starting with whatever the caller sees. These values usually live in three different repositories owned by three teams, which is most of the reason nobody has ever seen them side by side.

Layer (outermost first)Timeout sAttemptsBackoff sRemove
4 rows
Attempts means total tries, so 1 is no retry. Backoff is the wait between attempts at that layer.
This calculator runs entirely in your browser. Nothing you type is sent anywhere unless you ask for the result by email at the bottom of the page.
Where the ladder breaks
LayerTimeoutAttemptsOne attempt costsNeedsStatusAttempts that complete
API gateway30 s11.7 min1.7 minstarved0 of 1
Calling service25 s334 s1.7 minstarved0 of 3
HTTP client10 s311 s34 sstarved0 of 3
Database driver5.0 s25.0 s11 sstarved1 of 2
One attempt costs the full budget of everything beneath it. Needs is that multiplied by the attempts, plus this layer's own backoffs, uncapped by its own timeout since the timeout is the thing being checked.
Inversions
0
Inner timeout at or above its caller
Starved layers
4
Cannot finish inside the budget given
Stack needs
1.7 min
Outermost allows 30 s
Attempts asked for
18
1 fit the budget
The stack needs 1.7 min in the worst case and the outermost layer allows 30 s. Retries below it are cut off mid-flight.
For this configuration to hold, the outermost timeout would have to be 1.7 min. That is the honest arithmetic and it is rarely the right fix: 18 attempts for one request is the underlying problem, and retry counts multiply rather than add. Removing a retry layer costs nothing and divides the requirement.
4 layers cannot complete the retries they are configured for. Those attempts still consume capacity on the dependency and still count against its rate limit, so the configuration is buying load rather than resilience.
Send me this ladder

Your layer configuration goes with it. If retries below the top are being cut off mid-flight, that usually explains a class of failure everybody has been attributing to the dependency.

Your inputs are included so the reply can be specific.

Outermost timeout required, by retries and depth

What the top of the stack has to allow for every attempt beneath it to complete, given a two second leaf operation and one second of backoff per layer. Read across a row to watch the requirement explode, and note that the first row is flat: with no retries anywhere, depth costs nothing.

Attempts per layer1 layer deep2 layers deep3 layers deep4 layers deep5 layers deep
1 (no retries)2 s2 s2 s2 s2 s
25 s11 s23 s47 s1.6 min
38 s26 s1.3 min4.0 min12.1 min
411 s47 s3.2 min12.8 min51.2 min

Amber is over 30 seconds, red is over two minutes. Three attempts at four layers deep needs four minutes at the top for a two second operation, which is why the fix is nearly always fewer retry layers rather than a longer outer timeout.

A worked example

Four layers, each configured by a different team, each with a number that looks sensible in isolation. A 30 second gateway, a 25 second service with three attempts, a 10 second HTTP client with three attempts, and a 5 second database driver with two. Nobody has ever seen them in one place.

There are no inversions: the timeouts do decrease inward, which is the check most people would run and the only one that passes. The problem is underneath. One attempt at the HTTP client costs the driver's full budget of 10.5 seconds, one attempt at the service costs 33.5 seconds, and the stack as a whole needs 104.5 seconds against a gateway that allows 30.

So every one of the four layers is starved, and the number that makes it concrete is the attempt count: the configuration asks for 18 attempts and exactly one of them can complete inside the budget. The other seventeen are either never reached or cut off mid-flight, and the ones that do reach the dependency still consume its capacity on the way.

The arithmetic says the gateway would need 104.5 seconds. That is the honest number and it is the wrong fix: nobody waits a hundred seconds, and it would triple the load on a dependency that is already failing. Cutting retries alone is not enough either, because the leaf is expensive: dropping the service and client to two attempts each still needs 46 seconds. Two changes together do it. Take the driver timeout from five seconds to two, and the service and client to two attempts each, and the stack needs 22 seconds, which fits inside the existing 30 with headroom, at 8 attempts instead of 18. The ten retries that disappear were never completing anyway.

The arithmetic, so you can check it

Worked from the innermost layer outward. One attempt at any layer costs the full budget of everything beneath it, so a layer needs inner budget x attempts + backoff x (attempts - 1). The innermost has no child, so one attempt there costs at most its own timeout.

A layer is inverted when its timeout is at or above its caller's, which means it can never fire. It is starved when what it needs exceeds what its caller grants. Attempts that complete is floor((granted + backoff) / (one attempt + backoff)), capped at the attempts configured.

The honest limit

This computes the worst case, where every attempt runs to its full timeout. Real traffic mostly succeeds on the first attempt and never approaches these numbers, which is exactly why the configuration survives for years and then fails all at once during a dependency slowdown, when the worst case becomes the normal case. It also assumes the timeout is enforced on the whole attempt rather than on socket idle time, which differs by client and is worth checking on yours.

The multiplication and the retries nobody counts are in idempotency for money movement. For the deduplication window those attempts require, use the idempotency window calculator.

Questions

How should nested timeouts be set?

Decreasing inward, with each layer's budget large enough for the full inner budget times its own attempt count plus its own backoffs. The common mistake is setting each timeout independently to a number that sounds reasonable, which produces a ladder where the inner layers can never finish what they are configured to attempt.

Why do retry counts multiply rather than add?

Because each outer attempt re-runs the entire budget beneath it. Three layers at three attempts each is twenty-seven attempts for one request, and the time required grows the same way. Every individual layer is correctly inside its own limit, nothing is violated, and the total is a number nobody could state from memory.

What is a timeout inversion?

An inner timeout at or above the timeout of the layer calling it. The inner limit can then never fire, because the outer one always fires first, so every failure at any depth presents as the same outer timeout and the trace tells you nothing about where it happened.

My retries never seem to complete. Why?

Because the budget granted by the layer above is smaller than the time your attempts need. Those attempts still reach the dependency, still consume its capacity and still count against its rate limit, so the configuration is buying load rather than resilience. The tool reports how many of your configured attempts actually fit.

Should I raise the outer timeout or cut the retries?

Almost always cut the retries. Raising the top to accommodate a multiplied retry budget produces timeouts measured in minutes for requests a user is waiting on, and it does nothing about the load those attempts put on a dependency that is already struggling. Removing one retry layer costs nothing and divides the requirement.