Python signal.alarm SIGALRM Timeout Watchdog Prompt
Wrap stubborn blocking calls with a SIGALRM-based timeout context manager, fully aware of its main-thread and Unix-only caveats.
- Target user
- Senior Python engineers hardening automation against hung syscalls
- Difficulty
- Advanced
- Tools
- Claude, ChatGPT, Cursor
The prompt
You are a senior Python engineer adding a hard timeout around a blocking call that ignores cooperative timeouts (e.g. a C-extension call, a DNS lookup, or a library with no timeout parameter). Build a SIGALRM-based watchdog. Follow these steps exactly: 1. State the constraints up front in a docstring: signal.alarm/SIGALRM works only on Unix, fires only on the main thread of the main interpreter, and has whole-second granularity. If [TIMEOUT SECONDS] needs sub-second precision or the call runs off the main thread, recommend signal.setitimer or a process-based alternative instead and stop. 2. Implement a context manager (contextlib.contextmanager or a class with __enter__/__exit__) named timeout([TIMEOUT SECONDS]) that wraps the block in [PROTECTED CALL DESCRIPTION]. 3. On entry, save the previous handler with old = signal.signal(signal.SIGALRM, _handler) where _handler raises TimeoutError, then arm the timer with old_alarm = signal.alarm(int(ceil(seconds))). 4. On exit (in a finally), always disarm with signal.alarm(0) and restore the previous handler with signal.signal(signal.SIGALRM, old) so the watchdog never leaks across unrelated code or clobbers another alarm. 5. If a non-zero old_alarm was returned on entry, account for the pre-existing alarm: warn or reject nesting rather than silently swallowing the outer deadline, because only one SIGALRM timer exists per process. 6. Reject reentrancy/threading misuse: at __enter__ verify threading.current_thread() is threading.main_thread(); if not, raise RuntimeError with a clear message pointing the caller at concurrent.futures or a subprocess-based timeout. 7. Document that SIGALRM interrupts the blocking syscall by raising in the main thread, so the protected block must tolerate being interrupted mid-call and must not leave shared state half-written. Output format: return a single runnable Python module containing the timeout context manager, a tiny demo that wraps a sleeping/blocking call and catches TimeoutError, and a commented caveats section listing the main-thread/Unix-only/whole-second limitations. Idempotency/safety guardrail: __exit__ must restore the prior signal handler and clear the alarm unconditionally in a finally block, so entering and leaving the context any number of times leaves the process's signal state exactly as it was before — no orphaned timers, no replaced handlers.
Why this prompt works
Some blocking calls simply cannot be timed out cooperatively. A library that exposes no timeout= parameter, a C extension that blocks in native code, or a getaddrinfo lookup behind a misbehaving resolver will hang your automation indefinitely. signal.alarm is the classic Unix escape hatch: it schedules a SIGALRM that interrupts the blocking syscall and raises an exception in your handler. But it is sharp-edged enough that most engineers either get it subtly wrong or avoid it without understanding when it is the right tool. This prompt makes the model lead with the constraints, so the reader learns the boundaries — Unix-only, main-thread-only, whole-second granularity — before any code appears.
The structural risk with signal handlers is leakage. signal.signal and signal.alarm mutate global, process-wide state; a context manager that arms an alarm but fails to restore the previous handler and clear the timer will sabotage unrelated code later in the program. By insisting that __exit__ disarm the alarm and restore the saved handler unconditionally inside a finally, the prompt yields a watchdog that is perfectly stack-disciplined — the signal state after the block is byte-for-byte what it was before, which is the only way these are safe to use in a larger codebase.
The final two concerns are nesting and interruption semantics. Because there is exactly one SIGALRM timer per process, a nested timeout would silently clobber the outer deadline, so the prompt requires detecting and rejecting a pre-existing alarm. And since SIGALRM interrupts mid-syscall, the protected block must tolerate being unwound at an arbitrary point — meaning the watchdog is appropriate for read-only or atomically-committed work, not for code that leaves shared state half-written. Spelling all of this out is what separates a footgun from a reliable reliability primitive.
Related prompts
-
Python Context Manager Resource Cleanup Prompt
Wrap acquisition and guaranteed release of a resource (lock, temp dir, DB connection, mounted path) in a custom context manager so cleanup runs even on exceptions
-
Python Safe Subprocess Wrapper Prompt
Build a hardened Python wrapper around subprocess that runs external commands safely — no shell=True, list args, timeouts, captured output, non-zero handling, and streaming logs — replacing fragile os.system and shell-string calls.
-
Python Graceful Shutdown and Signal Handling Prompt
Add robust SIGTERM/SIGINT handling to a long-running Python worker so it finishes in-flight work, releases resources, and exits cleanly within the container's grace period instead of being SIGKILLed.