Every graceful shutdown guide for Kubernetes tells the same story. The pod gets SIGTERM. The endpoint leaves rotation. Traffic stops arriving. Your handler finishes the in-flight work and exits. It is a reasonable story. I wanted to know if it is true. Not plausible. Measured, with timestamps.So I built drainwatch. It is a small Go tool that holds open long-lived TCP and UDP connections against a pod, terminates the pod in different ways, and records what actually ends each connection and when. The evidence comes from Kubernetes watch events and from the packets themselves.I ran 25 trials. Five termination scenarios, five repeats each, one frozen binary, and a fresh kind cluster per scenario. Kubernetes v1.34.0, kube-proxy in iptables mode. Every claim below traces to a committed report.json in the repo.Finding 1: What you do on SIGTERM decides your TCP fate.Same cluster. Same trigger, a pod deletes. Same 30-second grace period. The only variable is what the application does when SIGTERM arrives.SIGTERM behaviorTCP outcome (all 5 repeats)Last TCP connection ends atContainer exitExit immediately10/10 severed by RST45 ms (30–98)0Drain, then exit10/10 closed cleanly25.04 s (25.03–25.11)0Ignore SIGTERM10/10 severed mid-stream30.06 s (30.05–30.06)137Values are medians across five repeats. Ranges are in parentheses.The application that exits immediately kills every connection it holds within 45 milliseconds. The application that drains keeps all ten connections alive to the end of its 25-second window and closes them cleanly. The application that ignores SIGTERM keeps its connections until the kubelet steps in. SIGKILL arrives within 62 ms of the 30-second grace boundary. Exit code 137, in all five repeats. An application that never exits does not choose when its connections die. The kubelet chooses.None of this contradicts the documentation. The next two findings cover what the documentation does not say.Finding 2: Your Service advertises dead pods.In the exit-immediately scenario, I measured the gap between two events. The moment the last TCP connection was severed, and the moment the endpoint was removed from the EndpointSlice.Metric (n=5)MedianWorst repeatLast TCP connection dead at45 ms98 msEndpoint removed at293 ms591 msWindow where the Service advertised a dead process221 ms521 msFor a median of 221 milliseconds, and over half a second in the worst repeat, the EndpointSlice named an endpoint whose process had already reset every connection it held. Any request routed on that advertisement had nowhere to land.This is the race that connection draining advice exists to cover. Now, it has a number. Both timestamps come from the same clock, so the window is exact. The window is computed within each repeat and the median taken across repeats, which is why it is not the difference of the two medians above.One more result from the same data. The common assumption is that endpoint removal happens some usable margin before SIGTERM. On this cluster, the two events were simultaneous to within measurement resolution. Every trial, every scenario.Finding 3: UDP has no drain.This finding surprised me, and I work on real-time media systems.The drain scenario ran the textbook graceful shutdown. Stop accepting new connections. Keep serving established ones. Exit when done. TCP got its 25 seconds. The UDP flows were held open against the same pod, through the same Service, and they were gone 2.4 seconds after the delete. Not dead. Answered by a different pod.Deleting a Deployment-managed pod makes the ReplicaSet create a replacement. UDP is connectionless. The moment the replacement was ready, kube-proxy steered the datagrams to it. The original pod was still running. Still healthy. Still serving TCP, with a median of 22.9 seconds left in its drain window. The application executed a perfect drain, and for UDP it bought nothing.I know the flows moved because the probe stamps its pod name on every reply. More on why that exists below.To isolate the cause, I added a control. Scale the Deployment to zero instead of deleting the pod. No replacement exists, so re-homing cannot happen. It does not. Instead, every UDP flow died with ICMP port unreachable, a median of 177 ms after the container exited. That is kube-proxy rejecting traffic to a Service with no endpoints. Eviction behaves like delete and re-homes the same way. The median difference between the two is 3 ms.So a UDP flow during pod termination has two possible fates. Silently migrated to a different backend while the original is still draining, or rejected outright. The trigger decides which one, based on whether a replacement pod exists. The shutdown handler decides nothing. If you run WebRTC media, game servers, DNS, or anything else where UDP is the product, your graceful shutdown drains the control plane and abandons the data plane.One note on mechanism. The steering behavior is my inference from the timestamps. drainwatch records flow fates and cluster events. It does not inspect kube-proxy rules or conntrack.The bug that almost hid the best finding.In the first cluster run, drainwatch reported the re-homed UDP flows as "survived the observation window". The client kept receiving acks and had no way to know they came from a different pod. The report said nothing happened to these flows. The truth was the opposite.That is why the probe now identifies itself on every heartbeat and ack. A flow answered by a new instance is classified as severed, with the old and new pod names in the record. It is also why I trust the rest of these numbers. The harness aborts on any precondition it cannot verify. It refuses to average over partial runs. Where it causes an outcome, it says so: the exit-immediately scenario forces RST through SO_LINGER=0 and logs that it did, instead of letting kernel buffering decide the result. A measurement tool that cannot catch its own errors is not a measurement tool.CaveatsThis is kind, not production. The nodes are containers on one machine, so absolute latencies do not transfer. The structure of the results is the finding: what ends which flows, and in what order. kube-proxy ran in iptables mode, and IPVS or eBPF dataplanes may behave differently. Cluster event timestamps are watch receipt times, so they are upper bounds. And the workload is a single replica by design, so every outcome is attributable to one pod.Run it yourself.git clone https://github.com/jaynirmal15/drainwatchcd drainwatch && make demo # kind cluster up, one trial, report on stdoutscripts/repeat5-matrix.sh # the full 25-trial matrix, ~30 minutesEvery trial writes a full JSON report: environment, timeline, per-flow outcomes, and clock notes. The raw reports behind this article are committed in the repo.v0.2 targets the two questions this version cannot answer. Attaching to your workload instead of the built-in probe, and measuring cloud load balancer deregistration timing, where the dead-endpoint window from Finding 2 should get larger.A shutdown handler is not a formality. For TCP, it is worth three orders of magnitude. For UDP it is worth nothing, which is a different problem, and now a measured one.Jay Nirmal is a senior software engineer working on real-time communication infrastructure. drainwatch is MIT licensed. Issues and contrary results from other dataplanes are welcome.