Stopping without dropping  / field guide
Practitioner field guide · 2026-09-16

The router finds out last: taking a server out of service without dropping requests

Every deploy deliberately retires servers that are still being sent work. The signal that kills the process and the update that stops traffic travel on different paths, at different speeds, with no acknowledgement between them. This guide reconstructs how Google, Kubernetes, AWS, Envoy, GitHub and Slack coordinate that removal, what broke when they did not, and why the most widely deployed fix in the industry is a timed pause.

32 ledger entries, 26 distinct sources 11 production systems 5 incidents Evidence through September 2026 Read: 25 min
01

The territory

Removing a server on purpose is a three-party coordination problem, and the three parties do not share a transaction.

80%
of terminations at Google get the graceful SIGTERM notice at all; the rest go straight to SIGKILL
300s
default deregistration delay on an AWS application load balancer, range 0–3600s
15–60s
of 502 responses per rolling restart reported against Google Cloud load balancers, with one pod replaced
600s
Envoy's default drain window; the old process is kept for 900s on hot restart

State the problem without naming a technology: a fleet serves requests behind a routing layer that picks which server gets each one. Some other system, a deploy pipeline, an autoscaler, an operator running maintenance, decides a particular server must go. Three parties now have to agree: the thing that decides, the process that holds in-flight work, and the routing layer that keeps choosing backends from a membership list it believes. Termination is delivered to the process directly, in microseconds. The membership update is replicated out to every proxy, kernel table and client that routes, in seconds to minutes. Nothing sequences the two. Every dropped-request-on-deploy incident in this guide is that gap, observed from a different angle.

This is not an exotic failure. It runs on every rolling deploy, every scale-down and every node drain, which is why the error rate it produces is small, periodic and easy to normalise. The Kubernetes documentation states the race as designed behavior: endpoint removal is evaluated “at the same time as the kubelet is starting graceful shutdown”, with no ordering guarantee. Rakuten's engineering blog says it plainly: there is no orchestration between sending the termination signal and removing the pod from the endpoint list; the two can happen in any order.

The surprise this dig turned up sits at the end of that story. After a decade in which practically every Kubernetes team independently rediscovered the same workaround, a sleep in the preStop hook, the project made the workaround a first-class API field: KEP-3960 shipped a native sleep action, stable in v1.34 (2025), partly because hardened images had no sleep binary to exec. The platform's durable answer to a distributed race is a timer, and that is not an accident or an embarrassment; sections 2 and 3 argue it is the honest design once you refuse to put a synchronous barrier in the removal path. The second surprise is Borg reporting that the graceful path is only best-effort even inside Google: the SIGTERM notice arrives about 80% of the time. Whatever you build here is a latency optimisation layered on crash-safety, never a substitute for it.

Scope: this guide covers the deliberate removal path, deploys, scale-downs, drains and proxy reloads, for stateless request-serving fleets. It does not cover deciding that an unresponsive peer is dead (the detection problem is a separate guide in this collection), nor stateful failover, nor draining batch workers from a queue.

Figure 1 · Three parties, two paths, no acknowledgement

Server being removed

Routing plane

Control plane

kill signal:
microseconds, direct

membership update:
seconds, replicated

missing edge: no removal
acknowledgement flows back

keeps routing until
its copy catches up

pinned by
open connections

Orchestrator /
deploy tooling

Proxies, kube-proxy,
cloud load balancers

Clients holding
long-lived connections

Process with
in-flight requests

Server being removed

Routing plane

Control plane

kill signal:
microseconds, direct

membership update:
seconds, replicated

missing edge: no removal
acknowledgement flows back

keeps routing until
its copy catches up

pinned by
open connections

Orchestrator /
deploy tooling

Proxies, kube-proxy,
cloud load balancers

Clients holding
long-lived connections

Process with
in-flight requests

The kill path is fast and direct; the membership update is eventually consistent; and no acknowledgement flows back from the routing plane to the kill path. Sources: Kubernetes pod lifecycle docs, KEP-1669.
Diagram source
02

How it is actually built

Five systems that published their removal path converge on one sequence. They differ on exactly one axis: how the waiter learns it is safe to stop.

Google's RPC stack, Envoy, the AWS managed load balancers, Kubernetes and the gRPC protocol each solve this with what is recognisably the same five-step sequence, assembled from different parts. First, an intent signal reaches the server: SIGTERM in Borg and Kubernetes, an admin drain endpoint in Envoy, a deregistration call at AWS. Second, the server stops being selectable while it is still serving. Google's SRE book names this state the lame duck: “the backend task is listening on its port and can serve, but is explicitly asking clients to stop sending requests”. Kubernetes expresses the same state through the API since KEP-1672: a terminating endpoint stays visible with serving=true, ready=false. An AWS target enters draining. Third, the removal propagates through the routing plane. Fourth, in-flight requests finish, and persistent connections are told to leave at the protocol layer, HTTP/2 GOAWAY or Connection: close, because a connection that outlives the drain window pins its client to the dying server. Fifth, a hard deadline fires regardless: SIGKILL when the grace period expires, the deregistration delay elapsing, Envoy's --parent-shutdown-time-s killing the old process at 900 seconds. Every published implementation has all five steps; none skips the deadline.

Figure 2 · The reference removal sequence

ClientsRouting planeServerOrchestratorClientsRouting planeServerOrchestrator3. propagation windowpush: 1-2 RTT. poll: probe interval x threshold.open loop: a configured timerbackstop: SIGKILL / delay expiryfireswhether or not draining finished1. intent to stop (SIGTERM /drain call)2. stop selecting me (lame duck /endpoint update / draining state)2b. GOAWAY / Connection close on persistent connections4. in-flight requests complete5. exit before the deadline
ClientsRouting planeServerOrchestratorClientsRouting planeServerOrchestrator3. propagation windowpush: 1-2 RTT. poll: probe interval x threshold.open loop: a configured timerbackstop: SIGKILL / delay expiryfireswhether or not draining finished1. intent to stop (SIGTERM /drain call)2. stop selecting me (lame duck /endpoint update / draining state)2b. GOAWAY / Connection close on persistent connections4. in-flight requests complete5. exit before the deadline
The common shape across Google's lame duck, Envoy's drain, AWS deregistration and Kubernetes termination. The propagation window in the middle is the whole game: its length is set by the slowest replica of the membership list. Sources: SRE book ch. 20, Envoy draining docs.
Diagram source

The divergence point is step 3, and it divides the field into three designs. Google closes the loop: a task entering lame duck broadcasts the fact to its active clients, and because inactive clients still send periodic UDP health checks, the SRE book reports the state propagating in one or two round trips. The subscription is why Google can drain in seconds. Health-check-driven systems leave the loop half open: nothing announces the removal, the routing plane discovers it on its next probe, so the window is the probe interval times the unhealthy threshold, plus programming time. KEP-1669 identifies exactly this pair of factors for the traffic-loss window on Kubernetes services with local traffic policy. And the third design does not close the loop at all: it waits a fixed time and assumes propagation finished. That is the preStop sleep, the ALB deregistration delay, Envoy's 600-second drain window. I am calling this third design the open-loop wait; the sources call it endpoint propagation delay, deregistration delay, or nothing at all.

It is worth being precise about why the open loop won in the Kubernetes ecosystem, because the paper trail is unusually complete. The ask to close the loop exists: issue #106476 requests that “the SIGTERM should only start after the ingress controller/LB has removed the target from the target group”, notes that readiness gates “lack support during pod deletion”, and has sat accepted-but-frozen since November 2021. What merged instead, across five years, is a set of mechanisms that soften the race without sequencing it: KEP-1672 keeps terminating endpoints visible in the API (v1.20), PR #97238 lets kube-proxy fall back to a terminating pod rather than drop the connection, explicitly described by its author as patching “a race condition between when a pod is killed and when the external load balancer notices” (v1.22 era, merged June 2021), and KEP-3960 makes the wait itself a primitive (v1.34). The record does not state why sequencing was rejected; my reading, and it is a reconstruction, is that a synchronous barrier on pod deletion would hand every ingress controller and cloud LB a veto over the kubelet's termination path, and the project consistently chose eventual consistency over that coupling. Third-party projects fill the gap from outside: pod-graceful-drain uses an admission webhook to intercept the deletion and hold the pod until deregistration, its README arguing that the sleep is fragile on containers with no shell “and it is ugly”.

Figure 3 · Five years of narrowing, never closing, the gap

2020 ยท KEP-1672
terminating endpoints stay visible,
serving and terminating conditions

2021 ยท PR 97238 merged
kube-proxy routes to terminating
pods instead of dropping

2021 ยท issue 106476 filed
ask: SIGTERM only after LB removal.
accepted, frozen, unimplemented

2023 ยท KEP-3960
sleep becomes a native
preStop API action

2025 ยท v1.34 GA
the open-loop wait is now
a platform primitive

2020 ยท KEP-1672
terminating endpoints stay visible,
serving and terminating conditions

2021 ยท PR 97238 merged
kube-proxy routes to terminating
pods instead of dropping

2021 ยท issue 106476 filed
ask: SIGTERM only after LB removal.
accepted, frozen, unimplemented

2023 ยท KEP-3960
sleep becomes a native
preStop API action

2025 ยท v1.34 GA
the open-loop wait is now
a platform primitive

Kubernetes' paper trail on the termination race: every merged change softens the race; the one proposal to sequence it remains frozen. Sources linked in section 6.
Diagram source

The announcement

The state that says serving-but-leaving. Broadcast to clients at Google; expressed as serving=true, terminating=true in the Kubernetes API; the draining target state at AWS; failing health checks on purpose in Envoy's drain sequence.

Runs this way at: Google, Kubernetes, AWS

The wait

Sized by how the loop closes: 1–2 RTT when clients subscribe, probe interval times threshold when they poll, a configured constant when nobody tells anybody. The constant is load-bearing: too short drops requests, too long stalls every deploy.

Runs this way at: Kubernetes, Envoy

The deadline

Graceful never means unbounded. SIGKILL after 30s by default on Kubernetes, 900s parent shutdown in Envoy, deregistration completing at 300s on an ALB whether or not connections finished. Borg's 80% figure says the deadline path runs constantly in production, not rarely.

Runs this way at: Kubernetes, Google Borg

03

The decisions that matter

Four forks, each with a recorded argument, and the condition that flips each one.

Decision 1: where does the wait live, the platform or the application?

Chosen
  • A platform-level pause before SIGTERM: the preStop sleep, now a native API action (KEP-3960)
  • Works for any workload with no code change; survives distroless images
Rejected
  • Sequencing SIGTERM behind LB acknowledgement: #106476, frozen since 2021
  • App-only SIGTERM handling: correct for in-flight work, cannot stop traffic that is still being routed in
Flips when
  • Your LB programming latency is variable rather than bounded: a fixed sleep sized for the median will lose to the tail, and a webhook that holds deletion until observed deregistration (pod-graceful-drain) becomes worth its operational cost

Decision 2: drain connections by waiting, or rotate them by protocol?

Chosen
  • For persistent connections, server-initiated rotation: HTTP/2 GOAWAY with a grace for outstanding RPCs, bounded connection age (gRFC A9)
  • Envoy sends Connection: close / GOAWAY during its drain window
Rejected
  • Waiting for natural close: Slack kept old HAProxy processes alive for many hours while websockets drained
  • Hard-closing at the deadline: converts a drain into a reconnect storm
Flips when
  • Connection lifetime is much shorter than your deploy interval: plain HTTP/1.1 with short keep-alives drains itself, and rotation machinery is complexity you do not need

Decision 3: invest in graceful shutdown, or go crash-only?

Chosen
  • Both, with the roles kept straight: crash-safety is the correctness mechanism, the drain is a latency and error-rate optimisation on top
  • Borg documents the graceful notice as best-effort, delivered about 80% of the time
Rejected
  • Pure crash-only, as argued by Candea and Fox (2003): one way to stop, by crashing. Correct about state, silent about the routing plane; a crash drops whatever the router was still sending you
Flips when
  • Nothing flips the crash-safety half; every fleet takes ungraceful kills weekly. The drain half flips off when clients retry idempotently and cheaply, which makes dropped-at-deploy requests invisible and the drain window pure deploy latency

Decision 4: for the proxy tier itself, drain the box or hand over the socket?

Chosen
  • Socket handover: GitHub's multibinder holds LISTEN sockets in a broker and passes them over a UNIX socket; Cloudflare's tableflip encodes the same contract for Go services
Rejected
  • Dropping SYNs during the reload window and letting clients retry, the standard HAProxy technique GitHub outgrew; Yelp's variant delayed SYNs with qdiscs instead
  • Envoy's cross-version handover via SO_REUSEPORT: proposed 2018, still open
Flips when
  • Reload frequency is low and connection volume modest: the SYN-retry window is milliseconds, and at small scale nobody notices. GitHub's stated reason for multibinder was that at their scale a customer-impacting number of connections hit every window

Figure 4 · Choosing a drain design

yes: self-hosted proxies,
subscription RPC stack

no: managed LB,
fleet of kube-proxies

yes

no

yes

no

Can the routing plane
acknowledge removal
to the kill path?

Closed-loop drain:
announce lame duck,
wait for ack, exit

Do connections live
longer than a
deploy cycle?

Protocol rotation:
GOAWAY, bounded connection age,
jittered reconnects

Can you size the
propagation lag with
a measured bound?

Open-loop wait:
native sleep sized to the
observed programming tail

Hold the deletion:
webhook releases the pod only
on observed deregistration

yes: self-hosted proxies,
subscription RPC stack

no: managed LB,
fleet of kube-proxies

yes

no

yes

no

Can the routing plane
acknowledge removal
to the kill path?

Closed-loop drain:
announce lame duck,
wait for ack, exit

Do connections live
longer than a
deploy cycle?

Protocol rotation:
GOAWAY, bounded connection age,
jittered reconnects

Can you size the
propagation lag with
a measured bound?

Open-loop wait:
native sleep sized to the
observed programming tail

Hold the deletion:
webhook releases the pod only
on observed deregistration

The first question is whether anything can acknowledge removal back to the kill path; everything else follows from answering it honestly. Terminal boxes are designs that appear in section 2's sources.
Diagram source
DecisionChosenRejectedBecauseEvidence
Where the wait livesPlatform pause before SIGTERMBarrier on deletion awaiting LB ackA barrier gives every LB controller a veto over termination; eventual consistency was kept#106476, KEP-3960
Traffic to terminating podsRoute to serving-but-terminating as fallbackDrop on the floorDropping guarantees the error; the terminating pod usually still servesPR #97238, KEP-1669
Persistent connectionsServer-initiated GOAWAY + max ageWait for natural closeL4 balancers cannot move load off a connection that never closesgRFC A9
Shutdown philosophyCrash-safe core, drain on topDrain as the correctness mechanismThe notice is best-effort; one in five Borg terminations gets noneBorg §2.3, Candea & Fox
Proxy-tier reloadsSocket inheritance across processesDrop or delay SYNs in the windowAt large scale every reload window catches customer connectionsmultibinder, GLB part 2
Sizing the LB drain delayBound it near p99 request durationKeep the 300s defaultThe delay is a floor on deploy speed; AWS ends it early only when connections are goneAWS ELB docs
The transferable rule

Decide explicitly which of the three loop designs you are running: subscription, polling, or open-loop timer. Most outages in section 4 happened to teams that believed they were running one design while actually running another; the sleep that everyone copies is correct exactly when its constant is derived from a measured propagation tail, and folklore when it is copied from a blog post.

04

What broke in production

The published record sorts into three failure classes, and none of them is the drain sequence itself misfiring.

Class one: the rumor travels slower than the kill. This is the base race of figure 1 observed live, and it is the most-reported class by far, three independent trackers in this corpus alone. Class two: the membership ledger itself is wrong, so no amount of waiting helps; Slack's May 2020 outage and GitLab's March 2022 incident are both here, and both were triggered by routine capacity operations, not by deploys. Class three: the removal tooling bypasses the drain contract entirely; the machinery existed, and the script did not use it. That the mechanism executing as designed appears nowhere in the incident record as a cause is itself a finding: the drain sequence is not where the risk lives. The risk lives in the state it depends on and the tooling that is supposed to invoke it.

Figure 5 · The race, as it appears in every class-one report

ClientLoad balancerEndpoint controllerApp containerkubeletClientLoad balancerEndpoint controllerApp containerkubeletpar[kill path][removal path]detach observed complete15-60 s later in the GCP reportSIGTERM, delivered inmicrosecondsdetach endpoint, replicates insecondscloses listener onSIGTERMrequestrouted on stale membershipconnection refused, surfaced as 502
ClientLoad balancerEndpoint controllerApp containerkubeletClientLoad balancerEndpoint controllerApp containerkubeletpar[kill path][removal path]detach observed complete15-60 s later in the GCP reportSIGTERM, delivered inmicrosecondsdetach endpoint, replicates insecondscloses listener onSIGTERMrequestrouted on stale membershipconnection refused, surfaced as 502
SIGTERM lands in microseconds while the endpoint removal is still replicating; an app that closes its listener on the signal converts the race into connection-refused, which the edge surfaces as 502. Reconstructed from ingress-gce #2222 and linkerd2 #11084.
Diagram source
Issue record

One pod replaced, a minute of 502s

AssumptionA 20s preStop sleep, 120s grace and passing readiness probes were enough for a GCP load balancer routing directly to pods.
What happenedEach rolling restart produced 502s; the reporter stresses that “502 is returned even when only one pod is replaced, the second old pod is still alive”. Network endpoint group detach and reattach lagged the rollout.
Blast radius15–60 seconds of 502 responses per rolling restart, on every deploy.
FixNone accepted upstream; the issue was closed as not planned in the tracker.
Design ruleWhen the LB programs endpoints directly into cloud infrastructure, the propagation tail is minutes, not seconds; measure the detach latency before sizing any sleep, and treat surge-based rollouts as mandatory so old capacity outlives the tail.
Issue record

The sidecar makes the race three-way

AssumptionA service mesh in the pod would drain transparently during rollouts.
What happenedUnder a load generator, every rolling restart yielded 502s; logs showed “connection refused (os error 111)” before the proxy itself had received SIGTERM: the app died while its sidecar was still accepting on its behalf.
Blast radiusReproducible 502s on each deploy of the test service; duration bounded by rollout length.
FixTuning wait-before-exit annotations (25–60s) and close-wait timeouts; the thread documents mitigation, not a structural fix.
Design ruleAdding a proxy in the pod does not remove the race, it duplicates it: app and sidecar each need ordered shutdown relative to the other, and the pod spec is where that ordering has to be written down.
Postmortem

Slack: the membership ledger went stale for hours

AssumptionThe program syncing autoscaled instances into HAProxy's server slots would keep the routing state current.
What happenedA morning scale-up exhausted the pre-allocated slots; the sync program exited early when it found no free slot, so as the day's instances rotated, HAProxy's view of live backends drifted further from reality. The outage fired at the evening scale-down, the removal half of the day's churn.
Blast radiusSlack unreachable for customers 4:45–5:33 p.m. PDT on 2020-05-12, 48 minutes.
FixSlack's account describes repairing the slot management and, in the longer arc, replacing the reload-based HAProxy stack with Envoy.
Design ruleThe membership store is a stateful system with its own capacity limits and failure modes; monitor the divergence between it and ground truth directly, because every drain design in section 2 silently assumes that divergence is zero.
Postmortem

GitLab: a rename reconfigured the load balancer in place

AssumptionA chart change renaming service ports was routine and would roll out invisibly.
What happenedThe rename caused the GCP load balancer to be reconfigured live; requests timed out during the minutes each application pass took, across successive cluster rollouts.
Blast radius78 minutes of elevated errors and slowness on GitLab.com web, spread over five windows (9+33+8+7+21 minutes) on 2022-03-31.
FixCorrective actions included building graceful draining for entire regional clusters, so a cluster can be removed from service the way a single backend is.
Design ruleDrain machinery is usually built at one granularity, the instance. Incidents arrive at other granularities; if you cannot drain a whole cluster or zone through the same contract, the LB reconfiguration path is your untested removal path.
Postmortem

The cycler that skipped the contract

AssumptionAn autoscaling-group cycling script would replace instances the way the previous deploy tooling had, draining as it went.
What happenedA deploy combined a misconfigured private endpoint with what the author names “an unsafe ASG cycler script”; instances were cycled without safe draining and the site served 502 for all requests.
Blast radiusFull outage of the site until rolled back; a personal-scale system, which is exactly why the mechanism is legible.
FixThe postmortem's remedy is testing the cycler and correcting the endpoint configuration; the drain features themselves needed no change.
Design ruleEvery tool that can remove an instance, deploy pipeline, autoscaler, spot reclaimer, cost cleanup script, is a caller of the drain contract; enumerate the callers, because the one you forgot is the one that will skip it.
Issue record

ALB rollout errors that the sleep did not fix

AssumptionpreStop sleep plus 60s grace plus tuned health checks would produce clean ALB rollouts in IP-target mode.
What happenedUnder Fortio at 500 QPS, rolling updates produced 400s, 502s and 504s; targets were killed before deregistration completed, and no single knob eliminated it. The controller's durable answer became injected pod readiness gates for the registration side.
Blast radius0.7% + 0.2% + 0.1% of requests failed during the 60-second measured window of one rollout.
FixReadiness gates (mutating-webhook injected) hold the rollout until new targets are healthy; the deregistration side still rests on delay plus sleep.
Design ruleRegistration and deregistration are separate races with separate fixes; a green rollout dashboard proves the first one, and only an error-rate measurement under load proves the second.
Issue record

Client-side balancing re-derives the whole problem

AssumptionMoving load balancing into gRPC clients (headless service, client-side picking) would behave at least as well as the proxy it replaced.
What happenedRollouts produced UNAVAILABLE and connection-refused errors despite 60s preStop and 180s grace; the clients' view of the backend set, refreshed via DNS on a roughly 30s cycle, lagged the rollout exactly as an external LB's would.
Blast radiusErrors on each service rollout in the reporter's environment; closed as not planned upstream.
FixNone merged from the thread; gRFC A9's bounded connection age plus GOAWAY is the protocol's intended mechanism for exactly this.
Design ruleClient-side balancing moves the membership list into every client without changing its consistency: you now run ten thousand tiny load balancers, and the drain design must be one they all implement, which is what makes server-initiated GOAWAY the right lever.
05

Numbers you can plan against

Every value below is from the linked source; nothing is an estimate unless marked as one.

MetricValueAtContextAs ofSource
SIGTERM notice delivery rate~80%Google BorgShare of task terminations that get the graceful notice before SIGKILL2015Borg paper §2.3
Lame-duck propagation1–2 RTTGoogleBroadcast to active clients; idle clients learn via UDP health checks2016SRE book ch. 20
Default termination grace30 sKubernetesSIGTERM to SIGKILL; preStop overrun gets a one-off 2 s extension2026Pod lifecycle docs
LB deregistration delay, default300 sAWS ALBRange 0–3600 s; ends early only when no in-flight requests or active connections remain2026AWS ELB docs
Proxy drain window, default600 sEnvoy--drain-time-s; gradual strategy ramps drain pressure to 100% across the window2026Envoy CLI docs
Old-process lifetime on hot restart900 sEnvoy--parent-shutdown-time-s, the deadline behind the drain2026Envoy CLI docs
502 window per rolling restart15–60 sGCP NEG userOne pod replaced, 20 s preStop sleep, readiness passing2023ingress-gce #2222
Rollout error mix under load0.7 / 0.2 / 0.1%ALB userHTTP 400 / 502 / 504 shares at 500 QPS during one rolling update2019alb-controller #1065
Membership-state outage48 minSlackStale HAProxy slots; triggered by the evening scale-down2020Slack postmortem
LB-reconfiguration incident78 minGitLab.comFive windows of elevated web errors from an in-place LB reconfig2022GitLab #6736
LB-fleet rolling restart>1 hourGoogle MaglevWhole-cluster upgrade of the load balancers themselves, each drained beforehand2016Maglev paper
Websocket drain via old processesmany hoursSlackPre-Envoy: each HAProxy reload left the old process running until websockets closed2021Slack engineering

What to plan against: the deploy-time cost of removal is roughly the wait per instance times fleet size divided by rollout parallelism. With the ALB default of 300 seconds and serial batches, that is five minutes per batch before any application time; this is why the sizing decision in section 3 matters and why several sources cut the delay to 30 seconds or near their p99 request duration. That arithmetic is mine (derived, not reported). The measured figures above split cleanly: the Google numbers are primary measurements at scale; the 15–60 s and error-percentage figures are single-environment reports, valuable for shape rather than magnitude; and no source in this corpus publishes a fleet-wide error budget spent on deploys, which is an open number worth measuring in your own system.

Read these carefully

The AWS and Envoy values are vendor documentation of their own defaults, reliable as defaults but not as recommendations. The Borg 80% figure is from 2013 data published in 2015; treat it as an order of magnitude, corroborated by nothing newer, because nobody else has published an equivalent rate.

06

The evidence wall

Every source behind this page, graded. Filter by kind. The full claim-by-claim ledger ships alongside this file as sources.md.

How this was gathered, honestly

This dig ran in an environment whose network reaches github.com, gitlab.com and storage.googleapis.com directly; those sources were fetched in full. Sources on other hosts (marked “via search retrieval” in the ledger) were read through search-tool retrieval only, and their material is used as attributed reporting rather than verbatim quotation wherever wording could not be confirmed. No conference talk could be fetched at all, so the talk tier is empty: an access limitation of this run, not evidence that talks do not exist. KubeCon and SREcon programmes list several on this topic.

Postmortem Slack2020-05

A Terrible, Horrible, No-Good, Very Bad Day at Slack

HAProxy server-slot exhaustion made the routing state progressively stale all day; the 48-minute outage fired when the evening scale-down removed instances the state no longer tracked correctly.

Carry forwardMonitor divergence between the membership store and ground truth; every drain assumes it is zero.
slack.engineering
Postmortem GitLab2022-03

Elevated error rates across GitLab.com WEB service

A chart port-rename reconfigured the GCP load balancer in place; 78 minutes of degraded service across five windows. Corrective action: build graceful draining at cluster granularity.

Carry forwardIf you cannot drain a cluster through the same contract as an instance, the LB reconfig path is untested removal.
gitlab.com production tracker
Postmortem hyperbo.lan.d.

502s During Parameter Store Rollout

An untested ASG cycler script replaced instances without safe draining during a secrets migration; combined with a misconfigured endpoint, the site returned 502 for everything.

Carry forwardEnumerate every tool that can remove an instance; each is a caller of the drain contract.
hyperbo.la
Decision record Kubernetes SIG Node2023 → GA 2025

KEP-3960: Sleep Action for PreStop Hook

The design record that turned the community's sleep workaround into an API field, motivated by images with no sleep binary and by the endpoint-propagation race the sleep exists to lose deliberately.

Carry forwardThe platform's own answer to the race is a timer; size yours from measured propagation, then use the native action.
kubernetes/enhancements
Decision record Kubernetes SIG Network2020

KEP-1669: Proxy Terminating Endpoints

Names the traffic-loss window on rolling updates and its two governing factors, local endpoint count and LB probe interval; kube-proxy learns to use terminating pods rather than drop connections.

Carry forwardYour loss window is probe interval times threshold; you can compute it before you ever load test.
kubernetes/enhancements
Decision record Kubernetes SIG Network2020

KEP-1672: Tracking Terminating Endpoints

Before this, terminating endpoints vanished from the API entirely; the serving and terminating conditions make the lame-duck state expressible so consumers can drain instead of guess.

Carry forwardA drain design needs the serving-but-leaving state to exist in the API it reads; check yours has it.
kubernetes/enhancements
Decision record gRPC2017

gRFC A9: Server-side Connection Management

L4 balancers cannot move load off idle HTTP/2 connections, so servers bound connection age and drain with GOAWAY, letting outstanding RPCs complete inside a grace window.

Carry forwardFor persistent protocols, rotation is a server-side setting; you do not need to touch clients to fix drain.
grpc/proposal
Source Kubernetes2021-06

PR #97238: kube-proxy handle terminating endpoints

The merged implementation whose description states its purpose exactly: patch the race between when a pod is killed and when the external load balancer notices, by routing to terminating pods as a last resort.

Carry forwardFallback-to-terminating turns a guaranteed error into a usually-served request; prefer it wherever your proxy offers it.
kubernetes/kubernetes
Source Kubernetes2021-11, open

Issue #106476: termination kicks in before the ingress controller can react

The recorded, accepted, frozen ask to sequence SIGTERM behind LB deregistration and to extend readiness gates to deletion. Five years of activity around it produced softeners, never the barrier.

Carry forwardDo not wait for the platform to close this loop; the record says it will not, and your design must absorb that.
kubernetes/kubernetes
Source Google Cloud users2023-08

ingress-gce #2222: rolling restart yields a minute of 502s

The cleanest public measurement of cloud-LB endpoint programming lag: 15–60 seconds of 502s per rollout with only one pod replaced, closed as not planned.

Carry forwardDirect-to-pod cloud LBs put minutes-scale infrastructure programming inside your deploy loop; measure it.
kubernetes/ingress-gce
Source Linkerd users2023-07

linkerd2 #11084: consistent 502s during rolling deploys

With a mesh sidecar, the app can die while the proxy still accepts for it; connection refused before the proxy even saw SIGTERM. The thread is a catalogue of ordering annotations.

Carry forwardA sidecar doubles the shutdown-ordering problem; write the ordering into the spec, not into hope.
linkerd/linkerd2
Source AWS controller users2019-11

aws-load-balancer-controller #1065: rollout errors under load

Measured 400/502/504 mix at 500 QPS during a rolling update; no single knob fixed it, and the ecosystem's answer became webhook-injected pod readiness gates for the registration side.

Carry forwardProve deregistration with an error-rate measurement under load; the rollout dashboard only proves registration.
kubernetes-sigs
Source Envoy2018-07, open

Envoy #3804: hot restart across versions with SO_REUSEPORT

The recorded design argument about socket handover when shared-memory formats change; without it, “the only way to do this would be something like a prolonged cluster drain for upgrades”. Open since 2018.

Carry forwardSocket handover has version-compatibility edges; the fallback for the proxy tier is always fleet-level drain, so keep that path exercised.
envoyproxy/envoy
Source Cloudflare2018

tableflip: graceful process upgrades for Go

The handover contract stated as four constraints: no old code after success, an init grace for the new process, crashing during init is fine, one upgrade at a time.

Carry forwardThose four constraints are the acceptance test for any socket-handover scheme, including one you write yourself.
github.com/cloudflare/tableflip
Source GitHub2016-12

multibinder: LISTEN sockets as a shared service

A broker holds the LISTEN sockets and passes them over a UNIX domain socket, so HAProxy reloads reuse the same socket and the port never closes; identical binds get the identical socket.

Carry forwardSocket ownership can be separated from the serving process; that separation is what makes reloads a non-event.
github.com/github/multibinder
Source foriequal02021

pod-graceful-drain: hold the deletion instead of sleeping

An admission webhook intercepts pod deletion and delays it until deregistration has happened, because the sleep needs a binary the image may lack, needs chart patches everywhere, “and it is ugly”.

Carry forwardThe closed-loop alternative exists today as a webhook; weigh its operational surface against the sleep's imprecision.
github.com/foriequal0/pod-graceful-drain
Source gRPC Java users2024-07

grpc-java #11351: UNAVAILABLE during rollouts with client-side LB

Client-side balancing lagged rollouts on its DNS refresh cycle exactly as an external LB lags on probes; 60s preStop and 180s grace did not save it. Closed as not planned.

Carry forwardMoving the balancer into clients moves the stale-membership problem into every client; drain via server GOAWAY, not client tuning.
grpc/grpc-java
Paper Google2015

Large-scale cluster management at Google with Borg

Section 2.3: tasks can request SIGTERM notice before SIGKILL to finish requests and decline new ones, and in practice the notice arrives about 80% of the time.

Carry forwardOne in five removals gets no notice at the best-instrumented operator on record; crash-safety is the floor, not an option.
EuroSys 2015 (PDF)
Paper Google2016

Maglev: a fast and reliable software network load balancer

The balancer fleet itself is upgraded by rolling restart with per-machine draining, an operation that can run over an hour; consistent hashing plus connection tracking keeps connections alive while the balancer set churns.

Carry forwardThe routing plane has its own removal problem; consistent hashing is what makes its churn survivable for your connections.
NSDI 2016 (PDF)
Paper Candea & Fox, Stanford2003

Crash-Only Software (HotOS IX)

The counter-position: if software must recover from crashes anyway, crashing should be the only stop mechanism and recovery the only start. Right about state; the routing-plane half of removal is outside its frame.

Carry forwardBuild shutdown so that skipping it is safe; then the graceful path only buys latency and error-rate, which is exactly what you want it to buy.
usenix.org
Case study Google SRE2016

SRE book ch. 20: lame duck state

The reference description of serving-but-leaving: the task listens and can serve while explicitly asking clients to stop; the state reaches clients in one or two round trips and exists to make clean shutdown simple.

Carry forwardDrain latency is a function of who gets told; a subscription model turns minutes of polling into RTTs.
sre.google
Vendor docs Kubernetes2026

Pod lifecycle: termination of pods

The platform documents that endpoint removal is evaluated at the same time as shutdown begins, that deletes default to 30 seconds of grace, and that ordering is not guaranteed.

Carry forwardThe race is documented, intended behavior; design against the documentation, not against the behavior you wish it had.
kubernetes.io
Vendor docs Envoy2026

Draining, and the CLI defaults behind it

Protocol-aware drain: fail health checks, discourage traffic across the window (600 s default, gradual ramp), Connection: close for HTTP/1, GOAWAY for HTTP/2, old process killed at 900 s.

Carry forwardEnvoy's defaults are a published, battle-tested sizing of every knob this guide discusses; diff yours against them.
envoyproxy/envoy docs
Vendor docs AWS2026

Target group deregistration delay

Deregistering targets enter draining; the delay defaults to 300 seconds (0–3600), and completes early only when no in-flight requests or active connections remain.

Carry forwardThe default is a deploy-speed tax sized for slow requests you probably do not have; set it near your p99.
docs.aws.amazon.com
Vendor docs AWS2026

Pod readiness gates for ALB targets

The motivating outage in the docs: a rollout where the target group holds only Initial or Draining targets. A mutating webhook injects a readiness condition tied to target health; deletion-side coverage is explicitly absent.

Carry forwardReadiness gates close the registration race only; do not read their green state as proof your deregistration is safe.
aws-load-balancer-controller docs
Eng blog GitHub2016-12

GLB part 2: HAProxy zero-downtime, zero-delay reloads

At GitHub's scale, the standard reload trick, drop SYNs briefly and let clients retry, caught a customer-impacting number of connections every time; multibinder's socket inheritance removed the window instead of shrinking it.

Carry forwardA mitigation that shrinks the window scales linearly with traffic against you; removing the window does not.
github.blog
Eng blog Yelp2015-04

True Zero Downtime HAProxy Reloads

The pre-handover state of the art: delay SYN packets in a Linux qdisc during the reload so the new process answers them on release; an ingenious buffer where GitHub later built a bypass.

Carry forwardKernel-level buffering can paper over a reload window, at the price of added latency and a mechanism nobody on call understands at 3 a.m.
engineeringblog.yelp.com
Eng blog Slack2021-03

Migrating Millions of Concurrent Websockets to Envoy

Reload-based config changes meant every HAProxy reload spawned a new process while the old one lingered for many hours draining websockets; a fleet of half-retired processes was one of the stated reasons to move to Envoy's dynamic config and built-in drain.

Carry forwardWhen connections live for hours, drain time is your config-change latency; architecture that avoids the reload beats architecture that waits it out.
slack.engineering
Eng blog learnk8scurrent

Graceful shutdown and zero downtime deployments in Kubernetes

The community's canonical walkthrough of the race: endpoint propagation is eventually consistent across kube-proxies, ingresses and cloud LBs, unsequenced against SIGTERM, so delay the shutdown until routing catches up.

Carry forwardThe article most teams actually implement from; its advice is the open-loop wait, and its constant still needs your measurement.
learnkube.com
Eng blog Rakutenn.d.

Zero-Downtime Rolling Deployments in Kubernetes

An operator's independent statement of the mechanism: no orchestration exists between the TERM signal and endpoint-list removal, the two happen in any order, and the preStop wait exists to absorb that.

Carry forwardMultiple operators converge on the same reading of the same race; treat that convergence as the corroboration it is.
engineering.rakuten.today
07

Build a miniature, then productionise it

Six rungs from an evening's toy to a measured production posture. The line from toy to real crosses at rung four.

Kill it badly, and count

A minimal HTTP server behind any load generator. Kill it with SIGKILL mid-load and count failed requests at the client. This number is your baseline, and every later rung is judged by how much of it survives.

Done when: you can state failed requests per kill, from client-side measurement.  Teaches: errors are measured at the caller; the server's own logs cannot see a dropped connection.

Handle the signal, watch it not be enough

Add a SIGTERM handler: stop accepting, finish in-flight, exit. Repeat the kill. In-flight failures disappear; connection-refused errors remain, because the load generator, like a load balancer, keeps dialing an address nobody told it to stop using.

Done when: failures are all connection-level, none mid-request.  Teaches: the app can only fix the in-flight half; routing is someone else's state.

Put a router in front, find the window

Add a reverse proxy with active health checks. On SIGTERM, fail the health endpoint first, keep serving, exit after the proxy stops sending. Vary probe interval and unhealthy threshold and plot the error window against them.

Done when: the measured window tracks probe interval times threshold, as KEP-1669 predicts.  Teaches: polling drains are computable; you can size before you test.

Do it on a real orchestrator

Deploy on Kubernetes behind an ingress, run the load generator, and roll the deployment with and without a preStop sleep. Then replace the sleep with a webhook-based hold (pod-graceful-drain or equivalent) and compare error rates and rollout duration.

Done when: you have an errors-per-rollout and minutes-per-rollout table for all three configurations.  Teaches: the trade the whole industry is making, on your own numbers.

Add a connection that refuses to die

Add a websocket or gRPC stream client that holds its connection. Roll again and watch drains stall or connections drop at the deadline. Fix it with server-initiated rotation: bounded connection age plus GOAWAY, and jitter the client reconnects.

Done when: a rollout completes inside its budget with zero mid-stream drops and no reconnect spike.  Teaches: persistent connections turn drain into a protocol feature, not a timer.

Take away the notice

Borg's number says one in five terminations gets no graceful notice. Make your rollout tooling SIGKILL a random 20% of instances instead of draining them, continuously, and verify the client-side error budget and any state invariants still hold.

Done when: the fleet passes its error budget with the 20% ungraceful rate left on permanently.  Teaches: the drain is an optimisation; crash-safety is the contract, and this rung is the regression test for it.

08

Keep hunting

The queries that found this material, grouped by what they surface. The vocabulary is the value: lame duck, deregistration delay, terminating endpoints, preStop.

The race, as practitioners hit it

  • "graceful shutdown" 502 rolling deploy preStop sleep "we"
  • postmortem "rolling deploy" OR "rolling restart" 502 "we" -tutorial
  • "terminating" pods "still receive traffic" kubernetes

Design records and the argument

  • KEP sig-network terminating endpoints site:github.com
  • repo:kubernetes/kubernetes is:issue "SIGTERM" endpoint removal sequence
  • "lame duck" "explicitly asking clients to stop sending requests"

Incidents in public trackers

  • site:gitlab.com gl-infra production "incident review" drain OR deployment 5xx
  • repo:kubernetes/ingress-gce is:issue 502 rolling restart
  • status page incident "connection draining" root cause

The proxy tier's own removal problem

  • "zero downtime" haproxy reload SO_REUSEPORT OR qdisc "we"
  • envoy hot restart drain-time-s parent-shutdown-time-s
  • grpc GOAWAY "max connection age" rollout UNAVAILABLE
09

References

  1. Kubernetes SIG Node, KEP-3960: Introducing Sleep Action for PreStop Hook kubernetes/enhancements, 2023, GA v1.34 2025. Checked 2026-09-16.
  2. Kubernetes SIG Network, KEP-1669: Proxy Terminating Endpoints kubernetes/enhancements, 2020. Checked 2026-09-16.
  3. Kubernetes SIG Network, KEP-1672: Tracking Terminating Endpoints kubernetes/enhancements, 2020. Checked 2026-09-16.
  4. gRPC, gRFC A9: Server-side Connection Management grpc/proposal, 2017. Checked 2026-09-16.
  5. andrewsykim, kube-proxy handle terminating endpoints (PR #97238) kubernetes/kubernetes, merged 2021-06-29. Checked 2026-09-16.
  6. nirnanaaa, Pod Termination handling kicks in before the ingress controller has had time to process (issue #106476) kubernetes/kubernetes, 2021-11-17, open. Checked 2026-09-16.
  7. Rolling restart results in 502 for ~minute from LoadBalancer (issue #2222) kubernetes/ingress-gce, 2023-08-08, closed as not planned. Checked 2026-09-16.
  8. Consistent 502s during shutdown/rolling a deployment (issue #11084) linkerd/linkerd2, 2023-07-05. Checked 2026-09-16.
  9. 400/502/504 errors while doing rollout restart or rolling update (issue #1065) kubernetes-sigs/aws-load-balancer-controller, 2019-11-06. Checked 2026-09-16.
  10. bplotnick, RFC: Hot restart across hot restart versions with SO_REUSEPORT (issue #3804) envoyproxy/envoy, 2018-07-06, open. Checked 2026-09-16.
  11. Cloudflare, tableflip GitHub repository. Checked 2026-09-16.
  12. foriequal0, pod-graceful-drain GitHub repository. Checked 2026-09-16.
  13. GitHub, multibinder GitHub repository, 2016. Checked 2026-09-16.
  14. Getting gRPC unavailable during services rollout when using client side gRPC load balancing (issue #11351) grpc/grpc-java, 2024-07-01, closed as not planned. Checked 2026-09-16.
  15. Kubernetes documentation, Pod Lifecycle: termination of Pods kubernetes.io; fetched via the kubernetes/website source repository. Checked 2026-09-16.
  16. Envoy documentation, Draining envoyproxy/envoy repository. Checked 2026-09-16.
  17. Envoy documentation, command line options envoyproxy/envoy repository. Checked 2026-09-16.
  18. AWS Load Balancer Controller, Pod readiness gate kubernetes-sigs repository docs. Checked 2026-09-16.
  19. AWS, Edit target group attributes (deregistration delay) AWS documentation; content via search retrieval. Checked 2026-09-16.
  20. Verma et al., Large-scale cluster management at Google with Borg EuroSys 2015, PDF fetched in full. Checked 2026-09-16.
  21. Eisenbud et al., Maglev: A Fast and Reliable Software Network Load Balancer NSDI 2016, PDF fetched in full. Checked 2026-09-16.
  22. Candea and Fox, Crash-Only Software HotOS IX, 2003; abstract via search retrieval. Checked 2026-09-16.
  23. Google, Site Reliability Engineering, ch. 20: Load Balancing in the Datacenter sre.google, 2016; content via search retrieval. Checked 2026-09-16.
  24. GitLab, production incident #6736: Elevated error rates across GitLab.com WEB service gitlab.com production tracker, 2022-03-31, fetched in full. Checked 2026-09-16.
  25. Slack, A Terrible, Horrible, No-Good, Very Bad Day at Slack slack.engineering, 2020; content via search retrieval. Checked 2026-09-16.
  26. Postmortem: 502s During Parameter Store Rollout hyperbo.la, n.d.; content via search retrieval. Checked 2026-09-16.
  27. GitHub, GLB part 2: HAProxy zero-downtime, zero-delay reloads with multibinder github.blog, 2016-12-01; content via search retrieval. Checked 2026-09-16.
  28. Yelp, True Zero Downtime HAProxy Reloads engineeringblog.yelp.com, 2015-04; content via search retrieval. Checked 2026-09-16.
  29. Slack, Migrating Millions of Concurrent Websockets to Envoy slack.engineering, 2021-03-15; content via search retrieval. Checked 2026-09-16.
  30. learnk8s, Graceful shutdown and zero downtime deployments in Kubernetes learnkube.com; content via search retrieval. Checked 2026-09-16.
  31. Rakuten, Zero-Downtime Rolling Deployments in Kubernetes engineering.rakuten.today, n.d.; content via search retrieval. Checked 2026-09-16.