...
Back

Every Timeout Is a Product Decision Somebody Forgot to Make

A call without a deadline is a promise to wait forever. Somebody will decide how long your user waits — the library author, the kernel, or you.

Every Timeout Is a Product Decision Somebody Forgot to Make

Every Timeout Is a Product Decision 🕰️

Most network calls in most codebases have no explicit deadline. That does not mean there is no deadline — it means the deadline was chosen by a library default, or by the operating system's connection timeout, and those numbers were picked with no knowledge of your users, your screen, or what they are waiting for.

The default is frequently on the order of a minute or more. No interface anywhere should make someone wait a minute for a button to respond, so in practice every un-deadlined call is a user experience nobody designed.


The clearest case we hit

We once had a mobile screen that felt broken after login. Everything was slow — not the network, the app. The cause was a token refresh on a path that ran before almost everything else, with no timeout. On a good connection it took milliseconds. On a degraded one, where the identity provider's endpoint was slow or unreachable, it waited. And waited. The user saw an app that had simply stopped.

The fix was not to make the refresh faster. It was to decide how long this is worth waiting for, and to define what happens after that — in this case, an eight-second ceiling and a fallback to the cached credential. The right answer was available immediately; the code just never asked the question.

That is the general shape. The bug is not slowness. The bug is the absence of a decision about slowness.


How to pick the number

Work backwards from the interface, not forwards from the dependency.

What is the user looking at? A keystroke-driven suggestion has a budget of a few hundred milliseconds — past that it is useless even if it arrives. A screen transition has a couple of seconds. An explicit "generate this" action can hold attention much longer, provided the interface shows progress. A background sync can take as long as it likes, because nobody is waiting.

What happens when the deadline passes? This is the half that gets skipped. A timeout with no fallback converts slowness into an error, which is often worse. The useful question is what you can serve without the dependency: a cached value, a partial view, a queued retry with an honest "we will finish this in the background."

Does the total budget add up? If a request calls three services with three-second timeouts each, your worst case is nine seconds, plus whatever the client allows. Budgets compose; timeouts written independently never add up to a number anyone would have approved.


Deadline propagation is the part that makes it real

A per-call timeout is not enough. What you want is a deadline that travels with the request: the entry point establishes "this must finish by T," and every downstream call gets the remaining time rather than a fresh full allowance.

Without propagation, work continues after the caller has given up. The user got their error; your database is still executing a query for a response nobody will read. Under load this is exactly backwards — the system does its most useless work at the moment it is most constrained.

Most modern platforms have a first-class way to express this: a context with a deadline, a cancellation signal, a request-scoped abort. Use it as a real constraint, not as a formality — and make sure cancellation actually reaches the expensive part, not just the wrapper around it.


The rule worth adopting

Every outbound call has an explicit deadline, and every deadline has a defined fallback.

Enforce it where you can: a shared HTTP client that requires a timeout argument, a lint rule for the raw fetch or connect calls, a review question. The habit matters more than the specific numbers, because the numbers will be wrong at first and can be tuned — whereas an un-deadlined call is not a number that needs tuning, it is a decision that was never made, silently delegated to whoever wrote the default.