Timeouts and deadlines: the client left, the work carried on
A timeout bounds the waiting of whoever waits and says nothing to whoever works. Two things follow, and both are usually discovered in production: a chain of three services with a one-second timeout at every hop can take three seconds, and after the client leaves the whole chain keeps holding connections and workers.
Full technical treatment
TL;DR
A timeout bounds waiting, not work. It belongs to whoever waits, and nothing about it is communicated to whoever works: by giving up on the wait, the caller has stopped nobody. A deadline is a different quantity — not a duration but a point in time, shared by the whole chain. Yet it does not stop the worker by itself either: only if it reached the worker and only if the worker looks at it.
Hence the main consequence: after the client leaves, the work carries on, and it is not free. Measured on a chain of three services: the client left after 500 ms and all three finished their work anyway — the completed counter says 3. With a budget announced, the same chain had one service do the work: the second refused without starting, the third was never called at all — and the refusal reached the client in 401 ms against 501, before its own wait would have expired. All that time the work holds a connection and the worker busy inside it.
Beyond that is what separates knowing from having read. Per-hop timeouts do not add up to the client's: measured, three services each with a 1000 ms timeout on the next, none of them exceeded, and the whole chain took 1203 ms — while the three-second bound is not measured but derived from how the chain is built. The cost of continuing work in this run is exactly two things, a connection and a worker; a real service adds whatever the worker had already taken (a database connection, a slot in a pool, memory for the answer), and under load that is what turns a spike into a sustained failure: the system is busy with results nobody is waiting for. And one word, "timeout", names two different quantities: a connection timeout bounds establishing a connection, a response timeout bounds waiting for data, and setting one while believing you set both is the usual mistake.
- one service calls another over the network and waits for an answer;
- calls form a chain: the one that was called goes on to call the next one;
- whoever waits has finite patience: sooner or later it stops waiting and leaves.
- how a deadline differs from a timeout, and what cancelling work means;
SO_RCVTIMEO,ETIMEDOUT, "request budget", a connection timeout against a response timeout.
What is actually being asked
The ladder usually runs like this:
- "Why do you need timeouts?" — the warm-up.
- "What timeout would you set on a call to a neighbouring service?" — where it becomes clear whether the candidate separates a connection timeout from a response timeout.
- "The client timed out and left. What happens on the server?" — where the substance starts.
- "How does a deadline differ from a timeout?" — the central question.
- "Every hop has a one-second timeout, yet the request takes three. Why?" — the question about summing.
- "How do you cancel work that is no longer needed?" — answered differently in different languages, though the mechanism is one.
The numbers come from running bench/deadlines/chain.py and
bench/deadlines/practice.py over loopback. The 400 ms delays are set by the
script itself; what carries meaning here is not the milliseconds but the counters
of work done and work refused.
Base: three different questions about time
Take the most ordinary chain there is. Service A calls service B, B calls service C, and only then does the answer travel back. The client knocking on A is prepared to wait no more than a second: any longer and it simply leaves.
Three different things get asked about time in that chain, and in conversation they are constantly substituted for one another:
- a timeout — how long to wait. It is a duration, and it belongs to whoever waits: A decides for itself how long it is prepared to wait for B.
- a deadline — until what moment. Not a duration but a point in time: "the answer is needed no later than such-and-such an instant", and there is one of those for the whole chain, however many hops it has.
- cancellation — a request to stop working. That one is not about time at all but about a message: somebody has to tell the worker that the result is no longer wanted.
And here is the question this lesson is about: the client left — what happens to the work it ordered?
The answer the rest of the lesson grows out of: an expired timeout is an event only for the party that was waiting. It stopped waiting, and that is all. B and C learned nothing about it and keep computing: no message went to them, because a timeout is not a message.
A deadline does not close that hole by itself. A deadline is merely a value in the request, and it can stop the callee only under two conditions. First, it has to arrive: the protocol between A and B must carry it, or B will never know about it. Second, the callee has to honour it — to look at the time left before each step and refuse when there plainly is not enough. Take either condition away and the deadline stays a note on the caller's side, behaving exactly like a timeout.
That is already enough to answer the basic interview question. Everything below is about what continuing work costs, what a deadline that really did arrive looks like, and why one-second timeouts at every hop do not give one second for the whole chain.
Mechanism 1: a timeout belongs to the one who waits
Start with what a timeout is at the socket level:
Specify the receiving or sending timeouts until reporting an error … if no data
has been transferred and the timeout has been reached, then -1 is returned with
errno set to EAGAIN or EWOULDBLOCK … as if the socket was specified to be
nonblocking.
Note what the description does not contain: a single word about the other side. A timeout is the waiting party's decision to stop waiting. No message goes to the worker: it never learns that its result is no longer wanted.
Check it with a chain of three services. Each "works" for 400 ms, the client waits 500 ms:
1. THE CLIENT GAVE UP; THE WORK DID NOT
---------------------------------------
client timeout, ms 500
services in the chain 3
work each service does, ms 400
what the client got client timeout
how long the client waited, ms 501
services that did the work anyway 3
services that refused before starting 0
services never called at all 0
The client left. All three finished. Not one refused — nobody had told them anything.
Hence a cost that usually goes uncounted: after the client leaves, the work keeps holding a connection and a worker — exactly what this run shows. In a real service the worker also holds everything it had already taken: a database connection, a slot in a pool, memory. Under load that is what turns a short spike into a sustained failure — the system is busy computing results nobody is waiting for, and has nothing left for new requests.
Mechanism 2: a deadline travels with the request
A deadline is not "a better timeout" but a different quantity. A timeout is a duration each party counts from itself. A deadline is a point in time, shared by the whole chain and passed along with the request.
The difference shows on the same chain. The only change: the client announces a budget, and each service subtracts what it spent and passes the remainder on:
2. THE SAME CHAIN WITH A DEADLINE PASSED ALONG
----------------------------------------------
budget the client announced, ms 500
what the client got deadline
how long the client waited, ms 401
services that did the work anyway 1
services that refused before starting 1
services never called at all 1
Three differences from the first block, all of them practical.
One service did the work instead of three. The first one made it (500 ms of budget against 400 ms of work), the second saw a hundred milliseconds left and refused without starting — and the third was never called at all; the run counts these separately: one refused before starting, one never called.
Note what did not happen: the client did not receive two thirds of a result. It received nothing — only a refusal. The saving is not a partial answer but the fact that two services out of three did not spend 400 ms each on work nobody was waiting for.
The client got an answer rather than silence. deadline is a meaningful
refusal: it says the request did not fit the budget, rather than that the network
disappeared.
The answer came earlier: 401 ms against 501. A refusal by deadline happens the moment it becomes clear there is no way to make it — not when somebody else's patience runs out.
And now the thing this block makes easy to miss: what worked was not the deadline but the cooperation. On its own a deadline stops nothing — it is a value, not a command. The second service refused without starting because two conditions held at once: the remaining budget arrived with the request, and the service looked at it before taking the work on. Remove the first — the protocol does not carry a budget — and the second service works its 400 ms just as in the previous block. Remove the second — the budget arrives and nobody reads it — and the outcome is the same. Which is why "we introduced deadlines" means nothing by itself: it means only that the budget is passed at every hop and that every callee is obliged to check it.
Mechanism 3: why per-hop timeouts do not add up
The most common practical puzzle: "we have one-second timeouts everywhere, where did three seconds come from?" The answer is that every hop counts time from itself.
3. WHY PER-HOP TIMEOUTS DO NOT ADD UP TO THE CLIENT'S
-----------------------------------------------------
timeout each service sets for the next one, ms 1000
services in the chain 3
what the client got done
how long the whole chain took, ms 1203
longest any single hop waited, ms under 1000
Not one timeout was violated. Every service honestly waited less than a second. And the client waited 1203 ms, because the waits happen in sequence.
Hence the upper bound — but it must not pass for a measured one. The three seconds here are not measured, they are derived from how the chain is built: every hop gives the next one a 1000 ms timeout (a constant in the script), the waits do not overlap, so the total wait cannot exceed 3 x 1000 ms. The measurement gives 1203 ms — the chain fits inside the bound rather than reaching it.
Hence the rule worth answering with: a timeout bounds one wait, not a whole request. The bound on a whole request is called a deadline, and it is set once, at the entrance.
Hence, too, a way to review somebody's configuration: if a service sets outgoing timeouts equal to its own incoming one, the chain is already broken. An internal timeout must be smaller than the remaining budget, not equal to it.
Deeper: two different timeouts called by one word
The last level is about a trap in configuration. connect(2) documents a separate
error:
Timeout while attempting connection. The server may be too busy to accept new
connections.
That is the connection timeout — the time allowed for a connection to be established at all. It is about availability: the server is not answering the handshake, the queue is full (the previous lesson), packets are being lost.
The response timeout is a different thing entirely: the connection exists, the request is sent, and we are waiting for data. That one is about processing speed.
They get conflated constantly, and the price is this: a single five-second timeout "for everything" gives you a system that waits five seconds for an unreachable service and the same five seconds for a slow answer — while in the first case it is sensible to give up quickly and try another replica, and in the second it may well be worth waiting. The actual values depend on your network and your service — they do not follow from this measurement.
How to answer in an interview
Short answer: a timeout bounds the caller's waiting and says nothing to the worker, so the work continues after the client leaves; a deadline is a point in time that travels with the request, and a worker can refuse against it without starting. Measured on a chain of three services: without a budget all three finished the work, with a budget one did — the second refused without starting and the third was never called.
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
Three things separate a good answer. First, you say that per-hop timeouts do not add up to the client's: every hop counts from itself, and three one-second hops give three seconds — and you add that this is derived from how the chain is built rather than measured. Second, you name the cost of continuing work — a connection, a worker, and behind them everything the worker holds — and explain why under load that turns a spike into a failure. Third, you separate the connection timeout from the response timeout: they stand for different failures, and one shared number serves both of them badly.
And one qualification that shows you have actually deployed deadlines. A deadline does not stop the callee by itself: it stops it only where both conditions hold — the budget is passed at every hop, and every callee checks it before taking work on. A protocol that does not carry the budget, or a service that never looks at it, turns the deadline back into an ordinary caller-side timeout, with all the consequences of the first block.
Next they ask
How does the worker learn that the client is gone?
It does not, unless it is told. A timeout is the waiting party's decision, and on a socket it looks like that side ending its wait; no signal and no event reaches the worker.
It can find out indirectly — by discovering the connection is closed when it tries to write the answer. But that happens at the end of the work, when the resources are already spent. That is why cancellation is made explicit: a budget inside the request, checked by the worker before each step.
What do you do when the protocol carries no deadlines?
Shrink the timeout by hand at every hop: if a service has a second for an incoming request, it must give an outgoing call noticeably less — leaving room for its own work and for the answer. It is a poor substitute for a deadline, but it preserves the important property: total waiting does not grow with the length of the chain.
And it makes a useful review check: equal timeouts on the way in and on the way out are almost always a mistake. They mean the service promises the caller more than it can guarantee.
Does a deadline solve the problem completely?
Not completely: it was measured that one service still worked for nothing — the one whose budget was sufficient while the next one's was not. A deadline cuts off the tail of pointless work, not all of it.
Removing the loss entirely is impossible: to know whether the budget is enough you would have to know the cost of the remaining path, and nobody knows that in advance. So an estimate is added to the deadline: a service that knows its typical work takes 300 ms refuses immediately when 100 ms are left.
What timeout should a connection get?
A noticeably smaller one than the response, and for a different reason: a connection is either established quickly or not at all. Waiting longer here does not bring success closer — it only postpones trying another replica.
The mistake that follows is a single timeout "for everything". With it an unreachable replica costs as much time as a slow but living answer, although the reactions to those two cases should be opposite.
Common misconceptions
a timeout cancels the work on the server
It cancels nothing and announces nothing: a timeout is a property of the caller's waiting. Measured: the client left after 501 ms and all three services in the chain finished their work. A worker learns about the departure at best when it writes the answer — by which point the resources are spent.
a deadline is the same as a timeout under another name
A timeout is a duration counted by each party from itself; a deadline is a point in time, shared by the chain and carried with the request. Measured on one and the same chain: without a budget three services worked, with a budget one did, and the client got a meaningful refusal in 401 ms instead of silence at 501.
once a deadline is announced, the callee will stop
Only if two conditions hold. First, the deadline arrived — the protocol between caller and callee carries it. Second, the callee honours it — it looks at the remainder before taking work on. Both held in the measured run, which is why the second service refused without starting. Without either one, a deadline stays a value on the caller's side and behaves like an ordinary timeout.
with a one-second timeout at every hop the client waits at most a second
The waits happen in sequence and every hop counts time from itself. Measured: three services, each with a 1000 ms timeout on the next, none exceeded — and the whole chain took 1203 ms. The three-second bound is not measured but derived: the waits do not overlap, so three one-second hops give at most three.
wasted work costs nothing — the process is running anyway
It holds a connection and a worker to the very end, and with them everything the worker had already taken. Under load that is what turns a spike into a sustained failure: the system is busy with results nobody awaits, and new requests find no room.
a deadline removes useless work entirely
It removes the tail, not all of it: measured, one service out of three still worked for nothing — its budget was sufficient, the next one's was not. The cost of the remaining path cannot be known in advance, so services add their own estimate to the budget: "my work takes 300 ms, 100 are left, I refuse now".
there is one timeout: just set it high enough not to cut off good requests
There are two, and they are about different things. The connection timeout is about availability: connect(2) documents ETIMEDOUT as "Timeout while attempting connection. The server may be too busy to accept new connections". The response timeout is about processing speed. One timeout for both makes an unreachable replica cost as much as a slow answer, though the reactions should be opposite.
a service should pass on the same timeout it received
Then it promises the caller more than it can deliver: nothing is left for its own work or for sending the answer. An outgoing timeout must be smaller than the remaining budget — and when the protocol carries no deadline, it has to be shrunk by hand at every hop.
Practice
Two exercises. Answer first, then check against the real output: in both, the correct answer comes from a script's committed output rather than being written by hand.
Practice · predict the output
budget = int(CLIENT_TIMEOUT * 1000) without_deadline, worked_without, _, _ = run(budget_ms=-1) with_deadline, worked_with, refused, uncalled = run(budget_ms=budget) print(without_deadline) print(worked_without) print(with_deadline)
Practice · estimate
Knowledge check
A client timed out and left after 500 ms. What happens to the work on the server?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A timeout bounds waiting, not work. It belongs to whoever waits, and nothing about it is communicated to whoever works: by giving up on the wait, the caller has stopped nobody. A deadline is a different quantity — not a duration but a point in time, shared by the whole chain. Yet it does not stop the worker by itself either: only if it reached the worker and only if the worker looks at it.
- Hence the main consequence: after the client leaves, the work carries on, and it is not free. Measured on a chain of three services: the client left after 500 ms and all three finished their work anyway — the completed counter says 3. With a budget announced, the same chain had one service do the work: the second refused without starting, the third was never called at all — and the refusal reached the client in 401 ms against 501, before its own wait would have expired. All that time the work holds a connection and the worker busy inside it.
- Beyond that is what separates knowing from having read. Per-hop timeouts do not add up to the client's: measured, three services each with a 1000 ms timeout on the next, none of them exceeded, and the whole chain took 1203 ms — while the three-second bound is not measured but derived from how the chain is built. The cost of continuing work in this run is exactly two things, a connection and a worker; a real service adds whatever the worker had already taken (a database connection, a slot in a pool, memory for the answer), and under load that is what turns a spike into a sustained failure: the system is busy with results nobody is waiting for. And one word, "timeout", names two different quantities: a connection timeout bounds establishing a connection, a response timeout bounds waiting for data, and setting one while believing you set both is the usual mistake.
In fact
- It cancels nothing and announces nothing: a timeout is a property of the caller's waiting. Measured: the client left after 501 ms and all three services in the chain finished their work. A worker learns about the departure at best when it writes the answer — by which point the resources are spent.
- A timeout is a duration counted by each party from itself; a deadline is a point in time, shared by the chain and carried with the request. Measured on one and the same chain: without a budget three services worked, with a budget one did, and the client got a meaningful refusal in 401 ms instead of silence at 501.
- Only if two conditions hold. First, the deadline arrived — the protocol between caller and callee carries it. Second, the callee honours it — it looks at the remainder before taking work on. Both held in the measured run, which is why the second service refused without starting. Without either one, a deadline stays a value on the caller's side and behaves like an ordinary timeout.
- The waits happen in sequence and every hop counts time from itself. Measured: three services, each with a 1000 ms timeout on the next, none exceeded — and the whole chain took 1203 ms. The three-second bound is not measured but derived: the waits do not overlap, so three one-second hops give at most three.
- It holds a connection and a worker to the very end, and with them everything the worker had already taken. Under load that is what turns a spike into a sustained failure: the system is busy with results nobody awaits, and new requests find no room.
- It removes the tail, not all of it: measured, one service out of three still worked for nothing — its budget was sufficient, the next one's was not. The cost of the remaining path cannot be known in advance, so services add their own estimate to the budget: "my work takes 300 ms, 100 are left, I refuse now".
- There are two, and they are about different things. The connection timeout is about availability:
connect(2)documentsETIMEDOUTas "Timeout while attempting connection. The server may be too busy to accept new connections". The response timeout is about processing speed. One timeout for both makes an unreachable replica cost as much as a slow answer, though the reactions should be opposite. - Then it promises the caller more than it can deliver: nothing is left for its own work or for sending the answer. An outgoing timeout must be smaller than the remaining budget — and when the protocol carries no deadline, it has to be shrunk by hand at every hop.
What is covered
- What is actually being asked
- Base: three different questions about time
- Mechanism 1: a timeout belongs to the one who waits
- Mechanism 2: a deadline travels with the request
- Mechanism 3: why per-hop timeouts do not add up
- Deeper: two different timeouts called by one word
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
2 SOURCES
- socket(7), Linux man-pages 6.7Official documentation. What a socket timeout actually bounds: SO_RCVTIMEO "Specify the receiving or sending timeouts until reporting an error … if no data has been transferred and the timeout has been reached, then -1 is returned with errno set to EAGAIN or EWOULDBLOCK … as if the socket was specified to be nonblocking". The part that matters for the lesson: a timeout is a property of the WAITING on this side, and nothing about it is communicated to the other one.https://man7.org/linux/man-pages/man7/socket.7.html
- connect(2), Linux man-pages 6.7Official documentation. Why a connection timeout and a response timeout are different quantities: connect(2) documents its own ETIMEDOUT as "Timeout while attempting connection. The server may be too busy to accept new connections". One timeout bounds establishing a connection, the other bounds waiting for data, and confusing them is expensive.https://man7.org/linux/man-pages/man2/connect.2.html