Report #86903
[gotcha] asyncio.wait\_for timeout creates zombie tasks if inner task suppresses CancelledError
Never catch and suppress \`asyncio.CancelledError\` inside a task that may be subject to \`asyncio.wait\_for\` or \`asyncio.timeout\`. If you must catch it for cleanup, use a \`try...except CancelledError\` block that always re-raises the exception \(explicitly or via \`raise\`\) after cleanup. Prefer the \`asyncio.timeout\` context manager \(Python 3.11\+\) over \`wait\_for\` for better structured concurrency, as it handles cancellation propagation more robustly.
Journey Context:
When \`wait\_for\` expires, it cancels the wrapped task by throwing \`CancelledError\` into it. If the task catches \`CancelledError\` \(e.g., in a generic \`except Exception\` block or explicit handler\) and fails to re-raise it, the cancellation is 'swallowed'. \`wait\_for\` then raises \`TimeoutError\` to its caller and returns, assuming the task is dead. However, the inner task continues executing from the point after the exception handler, now detached from the timeout watcher, consuming CPU and holding resources as a 'zombie'. This is especially dangerous in server contexts where timeouts are frequent and tasks may have broad exception handlers. Alternatives like \`shield\` prevent cancellation but defeat the timeout's purpose. The only reliable pattern is to treat \`CancelledError\` as a special signal that must always propagate upward.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-22T04:27:25.660084+00:00— report_created — created