Kernel#trap
trap(signal, handler) -> handler The trap method registers a handler for operating system signals. Signals are notifications sent to your process by the OS or other processes, commonly used for graceful shutdown and handling interrupts.
Common signals
# INT - Interrupt (Ctrl+C)
trap(:INT) { puts "Interrupted!" }
# TERM - Termination request
trap(:TERM) { puts "Shutting down..." }
# KILL - Immediate termination (cannot trap)
# trap(:KILL) { } # Doesn't work!
# HUP - Hangup (often used for config reload)
trap(:HUP) { load_config }
# USR1, USR2 - User-defined signals
trap(:USR1) { do_custom_action }
Signals can be specified as symbols or strings, and the handler runs when the OS delivers that signal to your process. Note that KILL and STOP cannot be trapped: the OS enforces their behavior regardless of what your Ruby code registers.
Handler types
Block handlers
# Block as handler
trap(:INT) do
puts "\nStopping gracefully..."
exit 0
end
A block handler is the most common form and gives you full control over the response. The block receives no arguments because Unix signals carry no payload beyond the signal number itself, so the handler must rely on shared state or global variables.
String handlers
# Built-in handler names
trap(:INT, "IGNORE") # Ignore the signal
trap(:INT, "DEFAULT") # Default behavior
trap(:INT, "EXIT") # Exit immediately
String handlers provide convenient shortcuts for common behaviors. "IGNORE" tells the process to disregard the signal entirely, "DEFAULT" restores the original system behavior, and "EXIT" terminates the process immediately without running any at_exit hooks. These shorthand forms cover the most frequent cases.
Method handlers
def handle_signal
cleanup
exit
end
trap(:INT, method(:handle_signal))
Passing a method object lets you reuse an existing method as a signal handler without wrapping it in an extra block. This is useful when the handler logic is already defined as a named method elsewhere in the class or module.
Practical examples
Graceful shutdown
shutdown_requested = false
trap(:INT) { shutdown_requested = true }
trap(:TERM) { shutdown_requested = true }
loop do
break if shutdown_requested
process_requests
sleep 1
end
puts "Graceful shutdown complete"
The flag-based pattern keeps the signal handler minimal: it sets a boolean and returns, while the main loop checks the flag on each iteration. This avoids doing complex work inside the handler itself, which runs on the main thread.
Config reload
trap(:HUP) do
puts "Reloading configuration..."
load_config
puts "Configuration reloaded"
end
The HUP signal is conventionally used for configuration reloads in Unix daemons. Trapping HUP and re-reading config files lets you change settings without restarting the entire process, making it a very common pattern in long-running services and web servers.
Signal-based communication
# Parent sends signal to child
child_pid = fork do
trap(:USR1) { puts "Got signal!"; exit }
sleep
end
Process.kill(:USR1, child_pid)
Process.wait(child_pid)
Parent and child processes can communicate through signals even though signals carry no data payload. The parent sends a signal as a trigger, and the child’s handler performs a predefined action. This pattern is common in process supervision, job coordination, and inter-process notification systems.
Ignoring signals
# Ignore SIGCHLD (prevent zombie processes)
trap(:CHLD, "IGNORE")
# Or for specific operations
trap(:INT, "IGNORE") # Ctrl+C does nothing
Ignoring SIGCHLD prevents zombie processes by telling the kernel to reap child processes automatically. Ignoring INT makes the process immune to Ctrl+C, which is occasionally useful in critical sections that absolutely must not be interrupted mid-operation by a user.
Process groups
# Send to process group
Process.kill(:TERM, 0) # Signal all processes in group
Signaling process group 0 targets every process in the caller’s process group, which is a convenient way to terminate a group of related processes at once. This approach is used in job control systems and process supervisors that manage multiple children.
Important notes
# KILL and STOP cannot be trapped
trap(:KILL, "IGNORE") # Still kills the process!
The operating system enforces KILL and STOP at the kernel level, so no amount of trapping can prevent them. This is by design: these signals exist as a last resort to terminate or pause a process that has stopped responding to all other signals.
Ruby signal names
# Use symbols or strings
trap(:INT) {}
trap("INT") {}
trap(Signal.list["INT"]) {}
Signal.list returns a hash mapping signal name strings to their numeric values, which is useful for introspection and for looking up signal numbers dynamically at runtime without hard-coding numeric constants. You can pass symbols, strings, or integers to trap interchangeably.
Thread safety
# Signals are delivered to the main thread
# Ensure your handler is thread-safe
Signals are always delivered to the main thread, even in multi-threaded programs. This means your handler runs on the main thread and should avoid blocking operations or complex synchronization that could deadlock the process.
The trap method is essential for building dependable servers and daemons that respond gracefully to operating system signals.
Signal handler best practices
A signal handler should stay small and delegate real work to the main program flow. The most reliable pattern is a flag-based approach: the handler sets a boolean and returns immediately, while the main loop checks that flag on each iteration and performs any cleanup, reload, or shutdown steps in a predictable order.
# Good: the handler flips a flag; the main loop owns the real work
@shutdown = false
trap(:TERM) { @shutdown = true }
loop do
break if @shutdown
process_request
end
puts "Shutting down cleanly..."
This separation matters for several reasons. Signals are delivered to the main thread in Ruby, so a handler that does heavy work can block the entire process at an unpredictable moment. A handler that mutates shared state or holds locks can deadlock if the signal arrives during a critical section. By keeping the handler to a flag flip, you avoid these hazards and keep the control flow in one place where it is easier to reason about, test, and debug.
The same flag-based approach applies to reload signals like HUP. The handler sets a flag, and the main loop reads new configuration at a safe point between operations — never while a request is mid-flight or a database transaction is open:
@reload_config = false
trap(:HUP) { @reload_config = true }
loop do
if @reload_config
load_configuration
@reload_config = false
end
serve_request
end
Delegating shutdown and reload work to the main loop also makes the ordering explicit. You know exactly when resources are released, sockets are closed, and workers are stopped because those steps live alongside the rest of the application logic — not scattered across signal-triggered callbacks that fire at unpredictable times. A small bridge in the handler and a clear path in the main loop is easier to maintain than four separate near-identical sections that each restate the same advice.