Graceful shutdown: where the errors on every rollout come from
"A few errors during a deploy" is not a fact of nature but the consequence of one decision: what a process does when it receives SIGTERM. The lesson measures both branches on a real server under load: an immediate exit breaks every request in flight, draining breaks none and costs the remainder of the work already started.
Full technical treatment
TL;DR
Stopping a service is a sequence of actions, not an instant. A process told to stop can either vanish at once or first stop taking new work, finish what it has, and only then exit. The one that leaves at once destroys everything it was holding: the client gets a broken connection instead of an answer. Which of the two happens is decided by the process's own code, not by the platform.
Hence the main consequence: "a few errors on every rollout" is a property of the code, not a fate. Measured on one and the same server, under the same load and with the signal arriving at the same moment: an immediate exit gave 20 broken requests out of 20, an orderly stop gave 0 out of 20. The only difference is the handler.
Beyond that is what separates knowing from having read. An orderly stop is
two actions in order: close the listening socket first, then wait for what has
already been started; the other order does not converge under load. The price is
the remainder of the work already running, not the grace period: measured, 164 ms
against 2 with 300 ms of processing and the signal halfway through. Draining is
possible at all only because SIGTERM can be caught while SIGKILL cannot — and
that is where the measured zero has its boundary: 0 out of 20 came out on this
server, under this load, and because all the work in flight fitted inside the
grace period. A drain has to be bounded in time; whatever does not finish is
ended by SIGKILL, and then the breakage is back.
- a service is a process: something starts it and something eventually stops it;
- handling a request takes time — there is a gap between "the request arrived" and "the response was sent";
- rolling out a new version means the old one is stopped at some point.
SIGTERM,SIGKILL, grace periods, exit codes 143 and 137;- what a listening socket is and what the
acceptqueue is, where connections wait for the application.
What is actually being asked
The ladder usually runs like this:
- "What happens when a container is stopped?" — the warm-up, pointing back
to the track's first lesson:
SIGTERM, a grace period,SIGKILL. - "Why do rollouts produce errors?" — where the substance starts.
- "What is a graceful shutdown?" — usually answered with "wait for the requests to finish", without saying which ones.
- "In what order do you close things?" — the question that separates those who have done it.
- "What about long requests that do not fit the grace period?" — about the limits of the method.
- "And the load balancer?" — about the drain starting before the signal.
The numbers come from running bench/shutdown/drain.py and
bench/shutdown/practice.py: a real server on loopback, twenty concurrent
clients, 300 ms of work each, and a signal arriving halfway through it.
Base: a shutdown is a sequence, not an instant
Before talking about signals and draining it is worth naming, in ordinary words, what "stopping a service" even means — because every mistake in this topic comes from treating a shutdown as a single action.
A shutdown done properly is four actions in order:
- stop taking new work — new requests are no longer picked up, they go somewhere that will serve them;
- finish the work already in hand — the requests already being processed are carried through to a response;
- close what was being used — database connections, files, connections to neighbouring services;
- exit.
A process without that sequence performs only the fourth action. And here is the question this lesson is about: what happens to the work in hand if you jump straight to step four?
The answer is simple and unpleasant: nothing happens to it — it is simply gone. There is no process, so there is nobody to answer: a client that has already sent its request and is waiting gets a broken connection instead of a response. The request may have been half-done, and the client will never know.
That is where "a few 500s on every rollout" comes from. It is not a property of rollouts: it is what the process did with its own work at the moment it was asked to leave.
That is already enough to answer the basic interview question. Everything below is about how a process is told to stop at all, why the order of the first two actions matters more than it looks, and why all this politeness is given a finite amount of time.
Mechanism 1: what exactly gets broken
The first mode is what a process with no handler at all does (the track's first lesson), or one whose handler says "exit now":
1. EXIT ON SIGTERM, THE WAY A PROCESS WITHOUT A HANDLER DOES
------------------------------------------------------------
requests broken 20 of 20
time from SIGTERM to exit, ms 2
wait status: killed by signal 15
the same as a shell reports it 143
Twenty out of twenty. Not "a few 500s" but every request that was in flight at that moment. Their share of the total flow is small only because the moment is short: what breaks is not a percentage of requests but whoever happened to be inside the second of the shutdown.
Hence the first clarification worth making in conversation: "0.1 % errors per deploy" is not a reliability figure but a product of processing time and deploy frequency. The same code with requests twice as long produces twice the errors without changing anything in itself.
Mechanism 2: draining is two actions, not one
The second mode differs by its handler. It does two things in order:
- closes the listening socket — no new connections are accepted;
- waits for the work on the connections already taken to finish, and only then exits.
2. STOP ACCEPTING, FINISH WHAT IS STARTED, THEN EXIT
----------------------------------------------------
requests broken 0 of 20
time from SIGTERM to exit, ms 164
exit code 0
Zero out of twenty. And note the price: 164 ms against 2 — the remainder of the work that was already running. The signal arrived halfway through 300 ms of processing, so about half of it had to be waited out.
The boundary of that zero is worth naming here, or a wrong rule comes out of it. Zero broken is the result of this run: this server, this load, 300 ms of processing, and a grace period the work in flight fitted into with room to spare. "Draining breaks nothing" is not a property of draining but a description of the case where the work finished in time. The rule is written from the boundary: a drain carries through only what fits into the time it is given, which is why the drain itself must be bounded in time. The reason is just below.
The order of the two actions matters, and that is the substance of the interview question. Wait first and close the socket afterwards, and new connections arrive during the wait — the drain never ends while the load lasts. Close the socket and exit immediately, and the same requests break as in the first mode.
Why this is possible at all is known from the first lesson: SIGTERM can be
caught. The other half of the same rule is the method's boundary:
SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
So a drain works only inside the grace period. Whatever has not finished
will be cut off, and no handler changes that. Hence the cost of waiting
indefinitely: a handler that waits "as long as it takes" does not get more time,
it gets SIGKILL at the end of the period — and then everything still in flight
breaks. The zero broken requests rest not on the handler's patience but on the
work having finished before that moment.
Mechanism 3: closing the socket is not the same as refusing
A subtlety worth knowing: closing the listening socket stops new connections from being accepted, but connections already sitting in the kernel's queue do not disappear. From the previous lesson:
It extracts the first connection request on the queue of pending connections for
the listening socket.
Connections pile up in the kernel's queue, not the application's. So at the moment of shutdown three different groups of requests exist, and each is handled differently:
- in flight — the drain finishes them;
- in the accept queue but not yet taken — closing the listening socket discards them; the client sees a reset although the application never knew they existed;
- not yet arrived — they go to another replica, if the balancer already knows this one is leaving.
Hence a third action that has nothing to do with the process itself: take the replica out of rotation first, and only then stop it. Draining inside the process does not help against requests still being directed at it.
Deeper: work that does not fit the grace period
A drain is bounded by the grace period, so there is work that will not fit into it: long exports, streaming responses, large transactions. The method does not save those, and it is worth saying so yourself.
Three ways, each with its own price.
Allow the interruption. If the operation is idempotent and the client retries (the lesson on retries), a break costs one retry. This is the cheapest option and the most common one in practice.
Make it resumable. Answer in parts with a position marker, and the client continues from where it broke off. More expensive in code, cheaper in operation.
Move it out of the request. The long work goes into a background job and the request only enqueues it. Then stopping the process breaks nothing important — and restarting the job becomes a separate mechanism with properties of its own.
How to answer in an interview
Short answer: a graceful shutdown is two actions in order — stop accepting new
connections and finish the ones already taken — and it works only because
SIGTERM can be caught. Measured on one and the same server under identical
load: without a handler 20 requests out of 20 were broken, with draining zero,
and the cost of draining was 164 ms against 2 — the remainder of the work already
started.
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 name the order: the socket
closes first, otherwise the drain never ends under load. Second, you talk about
the balancer: a replica is taken out of rotation before the signal, because
draining does not cancel requests still being routed to it. Third, you know the
boundary: draining lives inside the grace period, and SIGKILL cannot be caught,
so long work must be made resumable or moved out of the request.
And one thing that is easy to overdo. Saying "with draining nothing ever breaks"
passes a single run off as a property of the method. Zero out of twenty came out
on one particular server, under one particular load, with work that fitted inside
the grace period. The precise phrasing is this: a drain carries through exactly
what fits into the time it is given — which is why the drain is bounded in time
itself, or SIGKILL ends it and the breakage comes back.
Next they ask
Which comes first: waiting, or closing the socket?
Close the listening socket first, then wait. The other order does not converge under load: while you wait for the current requests, new ones arrive, and the drain lasts as long as the traffic does.
In the measured draining mode the handler does exactly this: it closes the socket and then waits for the counter of active handlers to reach zero. That is where the zero broken requests and the 164 ms stop come from.
Is draining inside the process enough?
No: it has no effect on where the balancer sends new requests. While the replica is still listed as alive, connections keep arriving, and closing the listening socket turns them into resets.
So the rollout order is wider: take the replica out of rotation, let the balancer
notice, and only then send SIGTERM. Inside the process the drain covers its own
part — the work already started.
What happens to requests that do not finish within the grace period?
They are cut off: when the period expires SIGKILL arrives, and it cannot be
caught, blocked or ignored. No handler helps — the process simply ceases to
exist.
So for long work a drain is a postponement rather than a solution. What solves it is one of three things: idempotency plus a retry, a resumable response, or moving the work out of the request into a background job.
How much does draining slow a rollout down?
By the remainder of the work already started, not by the whole grace period. Measured: 164 ms against 2 with 300 ms of processing and a signal halfway through — about half of one request.
The reasoning error this corrects is "draining costs thirty seconds". Thirty seconds is the limit of waiting, not the price; the real price equals the duration of the longest request that was in flight.
Common misconceptions
a small percentage of errors during a rollout is unavoidable
Measured on one and the same server, under the same load and with the same moment of the signal: 20 broken requests out of 20 without draining and 0 out of 20 with it. The only difference is the SIGTERM handler, which makes this a property of the code rather than of rollouts. The zero is not guaranteed by the method, though: it came out because all the work in flight fitted inside the grace period. Whatever does not fit is cut off by SIGKILL.
a graceful shutdown means 'waiting for the requests to finish'
It is two actions, and the order decides everything: close the listening socket first, then wait. In the other order the drain never ends under load — new connections keep arriving while you wait.
draining costs the whole grace period
It costs the remainder of the work already started. Measured: 164 ms against 2 with 300 ms of processing and a signal halfway through. The grace period is a limit on waiting, not a price: the process leaves as soon as it is done.
with draining inside the process a replica can be stopped at any moment
Draining does not affect the balancer. While the replica is listed as alive, new connections arrive, and closing the listening socket turns them into resets. First take it out of rotation, let that propagate — and only then send the signal.
draining saves long requests too, you just need a larger grace period
Up to a point, yes, but the boundary is hard: when the period expires SIGKILL arrives, and it "cannot be caught, blocked, or ignored". Long work is made resumable or moved out of the request rather than waited for indefinitely.
exit code 0 on shutdown means everything went well
It means the opposite: the process did not leave on its own, it was killed by a signal. Measured: the immediate exit made the wait return -15 — death by signal fifteen, which a shell renders as 128 + 15 = 143 — and twenty broken requests; draining gave code 0 and zero broken. The code says nothing about requests — only about who ended the process and how.
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
abrupt = run("abrupt")
drained = run("drain")
print(abrupt["broken"])
print(drained["broken"])
print(abrupt["exit"])Practice · estimate
Knowledge check
A server is processing twenty requests and exits immediately on SIGTERM. How many requests break?
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
- Stopping a service is a sequence of actions, not an instant. A process told to stop can either vanish at once or first stop taking new work, finish what it has, and only then exit. The one that leaves at once destroys everything it was holding: the client gets a broken connection instead of an answer. Which of the two happens is decided by the process's own code, not by the platform.
- Hence the main consequence: "a few errors on every rollout" is a property of the code, not a fate. Measured on one and the same server, under the same load and with the signal arriving at the same moment: an immediate exit gave 20 broken requests out of 20, an orderly stop gave 0 out of 20. The only difference is the handler.
- Beyond that is what separates knowing from having read. An orderly stop is two actions in order: close the listening socket first, then wait for what has already been started; the other order does not converge under load. The price is the remainder of the work already running, not the grace period: measured, 164 ms against 2 with 300 ms of processing and the signal halfway through. Draining is possible at all only because
SIGTERMcan be caught whileSIGKILLcannot — and that is where the measured zero has its boundary: 0 out of 20 came out on this server, under this load, and because all the work in flight fitted inside the grace period. A drain has to be bounded in time; whatever does not finish is ended bySIGKILL, and then the breakage is back.
In fact
- Measured on one and the same server, under the same load and with the same moment of the signal: 20 broken requests out of 20 without draining and 0 out of 20 with it. The only difference is the
SIGTERMhandler, which makes this a property of the code rather than of rollouts. The zero is not guaranteed by the method, though: it came out because all the work in flight fitted inside the grace period. Whatever does not fit is cut off bySIGKILL. - It is two actions, and the order decides everything: close the listening socket first, then wait. In the other order the drain never ends under load — new connections keep arriving while you wait.
- It costs the remainder of the work already started. Measured: 164 ms against 2 with 300 ms of processing and a signal halfway through. The grace period is a limit on waiting, not a price: the process leaves as soon as it is done.
- Draining does not affect the balancer. While the replica is listed as alive, new connections arrive, and closing the listening socket turns them into resets. First take it out of rotation, let that propagate — and only then send the signal.
- Up to a point, yes, but the boundary is hard: when the period expires
SIGKILLarrives, and it "cannot be caught, blocked, or ignored". Long work is made resumable or moved out of the request rather than waited for indefinitely. - It means the opposite: the process did not leave on its own, it was killed by a signal. Measured: the immediate exit made the wait return
-15— death by signal fifteen, which a shell renders as128 + 15 = 143— and twenty broken requests; draining gave code 0 and zero broken. The code says nothing about requests — only about who ended the process and how.
What is covered
- What is actually being asked
- Base: a shutdown is a sequence, not an instant
- Mechanism 1: what exactly gets broken
- Mechanism 2: draining is two actions, not one
- Mechanism 3: closing the socket is not the same as refusing
- Deeper: work that does not fit the grace period
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
2 SOURCES
- signal(7), Linux man-pages 6.7Official documentation. Why a shutdown starts with SIGTERM rather than SIGKILL at all: in the signal table SIGTERM has the default action `Term` and can be caught, while of the other one the page says outright: "SIGKILL and SIGSTOP cannot be caught, blocked, or ignored". Draining is possible precisely because the first signal can be handled.https://man7.org/linux/man-pages/man7/signal.7.html
- accept(2), Linux man-pages 6.7Official documentation. Why "stop accepting" and "stop answering" are different actions: accept "extracts the first connection request on the queue of pending connections for the listening socket", so connections pile up in the kernel's queue whether or not the application reads them. Hence the order of a drain: close the listening socket first, then finish what has already been taken.https://man7.org/linux/man-pages/man2/accept.2.html