skip to content

Alarms and Interval Timers

Bounding a call that offers no timeout of its own by arming a timer that raises from inside a handler. It works only on the main thread, and it is the answer expected for a blocking compiled call.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

How do you bound a blocking call with signal.alarm when it takes no timeout?

level: juniorimportance: must knowfreq 42%

answer

  1. The kernel can interrupt you on a clock
  2. One signal whose default is fatal
  3. The handler raises; the call site unwinds
  4. Disarming belongs in a finally block
  5. Whole seconds, Unix only, main thread

basics

~10 s

Install a SIGALRM handler with signal.signal that raises an exception, call signal.alarm(seconds) just before the blocking call, and call signal.alarm(0) in a finally block. The handler's exception unwinds the blocked call.

solid answer

~40 s

`signal.alarm(n)` asks the kernel to send `SIGALRM` to the process after roughly `n` whole seconds. On its own that kills the process, because `SIGALRM`'s default disposition is terminate, so you first install a Python handler with `signal.signal(signal.SIGALRM, handler)` whose body raises — usually `TimeoutError`. CPython runs that handler in the main thread between bytecodes; when the guarded call is a blocking syscall the signal interrupts it and the exception propagates out of the call site. Always disarm in a `finally` with `signal.alarm(0)`, or the timer survives a fast success and fires later inside unrelated code. `signal.alarm` returns the seconds left on any previously armed alarm, so arming a second one silently replaces the first. It is Unix-only and whole-seconds only; `signal.setitimer` gives fractional seconds.

code

python · 13 lines
python
import signal

def on_alarm(signum, frame):
    raise TimeoutError("took too long")

signal.signal(signal.SIGALRM, on_alarm)
signal.alarm(1)
try:
    signal.pause()          # blocks until a signal arrives
except TimeoutError as exc:
    print("bounded:", exc)
finally:
    signal.alarm(0)         # disarm, including on the success path

go deeper

for a junior

Be ready to write the four lines from memory: install a handler that raises, arm with signal.alarm, guard the call, disarm with signal.alarm(0) in finally. Knowing that an unhandled SIGALRM kills the process is the detail that separates recall from having actually run it.

for a middle

Explain the mechanics: the handler runs between bytecodes in the main thread, the exception is what makes the deadline visible, and there is only one such timer per process. Be able to say why the finally block is load-bearing rather than tidy.

for a senior

Show the production judgement — restore the previous handler, never leave a stale alarm armed, and know that a blocking syscall is interruptible while a long compiled call is not. Say when you would use a call's own timeout argument instead.

for a principal

Own the policy question: an out-of-band signal is a last resort for calls that expose no deadline, and a codebase that sprinkles signal.alarm around is really telling you its I/O layer lacks timeouts. Decide where deadlines live and push them into the transport.

Plenty of calls in a Unix Python process have no timeout of their own: a compiled library entry point, a device read, a legacy client that exposes no deadline argument. The oldest and still the most common answer is to make the kernel interrupt you on a clock, and to turn that interruption into an ordinary Python exception. ### The three moving parts **1. A disposition that is not the default.** `SIGALRM` is signal number 14 and its default disposition is *terminate the process*. If you arm an alarm and never register a handler, the process dies when it fires — no traceback, no `finally` blocks, no `atexit` callbacks, and a shell exit status of 142 (128 + 14). So the first line of the pattern is always `signal.signal(signal.SIGALRM, on_alarm)`. **2. A handler that raises.** The handler receives `(signum, frame)` and does one useful thing: `raise TimeoutError(...)`. It is the *raise* that produces the timeout — `signal.alarm` itself never raises anything. Because CPython only runs Python-level signal handlers between bytecode instructions in the main thread, the exception surfaces at whatever bytecode boundary the main thread reaches next. For a blocking syscall that boundary comes almost immediately: the signal interrupts the syscall, the interpreter notices the pending handler, runs it, and the exception propagates out of the call site as if the call had raised. **3. Arming and disarming.** `signal.alarm(n)` schedules the signal for approximately `n` seconds from now; `signal.alarm(0)` cancels a pending one. The argument is an integer — `signal.alarm(0.5)` raises `TypeError`, because whole seconds are all the underlying call accepts. The return value is the number of seconds remaining on the previously scheduled alarm, or `0` if none was armed, which is the only bookkeeping the API gives you. ### Why the finally block is not optional There is exactly **one** of these timers per process. Arming a five-second deadline around a call that finishes in forty milliseconds leaves 4.96 seconds on the clock. Nobody cancelled it, so it fires later — inside a different function, inside cleanup code, inside a long test run — and raises `TimeoutError` from a stack frame that never asked for a deadline. That is one of the classic ways this pattern turns into a mystery bug, and the cure is mechanical: arm in `try`, disarm in `finally`. The same one-timer-per-process fact explains why nesting does not work naively. An inner deadline overwrites the outer one; when the inner region exits and calls `signal.alarm(0)`, it has also cancelled the outer deadline. A serious helper saves the returned remaining time and restores it, or refuses to nest. ### What it can and cannot interrupt The mechanism is only as good as the interpreter's chance to run the handler. A blocking *syscall* is interrupted by the signal, so the eval loop gets control back quickly and the exception lands where you want it. A long **compiled** computation that neither returns to the interpreter nor checks for pending signals is a different story: the handler cannot run until that call finally returns, so the deadline is simply late — sometimes uselessly so. The C-level handler CPython installs does almost nothing but set a flag; all the real work waits for the eval loop. ### The shape to write ```python import signal def on_alarm(signum, frame): raise TimeoutError("sensor read exceeded its budget") previous = signal.signal(signal.SIGALRM, on_alarm) signal.alarm(3) try: result = read_from_device() # no timeout argument of its own finally: signal.alarm(0) signal.signal(signal.SIGALRM, previous) ``` Capturing and restoring the previous handler matters once more than one piece of code in the process cares about `SIGALRM`; `signal.signal` returns the old handler for exactly that reason, and it may be `signal.SIG_DFL`. ### Limits worth saying out loud This is a Unix-only mechanism — there is no `SIGALRM` on Windows. It is whole-seconds only; `signal.setitimer(signal.ITIMER_REAL, 0.25)` is the fractional-resolution sibling and shares the same underlying timer. And it only works from the process's main thread, because that is where handler registration is allowed and where the handler runs. If the call you want to bound already accepts a deadline of its own, use that instead: a real timeout argument beats an out-of-band signal every time. `signal.alarm` is the tool for the calls that offer you nothing.

  • What happens if you arm signal.alarm(3) but never install a SIGALRM handler?
    The default disposition for SIGALRM is to terminate the process, so it simply dies when the timer fires. There is no traceback, `finally` blocks and `atexit` callbacks do not run, and the shell reports exit status 142, which is 128 plus signal number 14. Installing a handler is the first line of the pattern, not an optional refinement.
  • Why does signal.alarm return a number, and what happens if two deadlines nest?
    It returns the seconds still pending on the previously armed alarm, or 0 if none. There is one such timer per process, so an inner deadline overwrites the outer one, and the inner region's `signal.alarm(0)` then cancels both. A helper that must nest has to save that returned remainder and re-arm it on the way out.
  • Can this pattern interrupt a long compiled computation that never returns to the interpreter?
    No. CPython's C-level handler only sets a flag; the Python handler runs between bytecodes in the eval loop. A compiled call that neither returns nor checks for pending signals keeps going, and the exception appears only once it finishes. The pattern works well for blocking syscalls, and poorly for CPU-bound compiled work — there a child process you can kill is the honest answer.

It is a kitchen timer wired to the fire alarm: useful only because you first decided what the bell should do, and dangerous if you walk out of the kitchen without winding it back to zero.

saying these in an interview costs you the question

  • Thinks signal.alarm itself raises the timeout exception
  • Forgets signal.alarm(0), leaving a stale timer armed
  • Believes an unhandled SIGALRM is quietly ignored
  • Passes a float to signal.alarm and expects sub-second timing
  • Assumes the pattern also works on Windows
  • Never restores the previously installed SIGALRM handler

context

open as a page

Why can't signal.alarm time out a blocking call running in a worker thread?

level: middleimportance: should knowfreq 33%

basics

~20 s

CPython runs every Python signal handler in the main thread. The SIGALRM handler's exception unwinds the main thread's stack, while the worker thread stays blocked exactly where it was. Signals cannot interrupt one specific thread.

open as a page

A telemetry collector arms signal.alarm per sensor read; why do descriptors leak and unrelated code raise TimeoutError?

level: seniorimportance: should knowfreq 26%

basics

~20 s

Almost certainly the alarm is never disarmed on the fast path, so it fires later in code that never asked for a deadline. And because the exception lands at an arbitrary bytecode boundary, it can arrive after a socket is opened but before anything is responsible for closing it.

open as a page

What does signal.setitimer with signal.ITIMER_REAL do that signal.alarm cannot?

level: middleimportance: nice to knowfreq 18%

basics

~10 s

signal.setitimer takes float seconds, so it can arm a sub-second deadline, and an optional interval argument that re-arms the timer for a repeating SIGALRM. signal.alarm is one-shot and whole-seconds. signal.getitimer reports what is left.

open as a page