At 02:00 the deployment script reported success. The log showed the worker finishing, the health check passing, and the script exiting with code zero. What the log did not show was the child process it had started ten minutes earlier, still alive, still holding the port, still appending to a log file that had already been rotated out from under it. The next deploy failed because the port was taken, and the postmortem said the obvious thing: the previous run did not clean up. Nobody had typed a wrong command. The script had simply killed the wrong thing.
Every language that starts a subprocess ships a kill button, and in every language that button is a lie half of the time. In Python the lie has three parts, and they fail in a fixed order: what terminate() actually sends, who is listening when the signal arrives, and what happens to the process after the signal lands. Most cleanup bugs live in the gap between the call and the corpse, and most of them survive code review because the happy path — a child that exits on its own — never exposes them. This article walks that gap from the first signal to the reaped process, and ends with a termination protocol that leaves nothing running, plus tests that prove it without a single sleep.






