Signal handling in Ruby gives you control over how your application responds to operating system signals. These signals notify your program about events like termination requests, user interrupts, or custom inter-process messages. Managing signals properly ensures your application shuts down cleanly, responds to system events appropriately, and maintains data integrity.
Operating systems use signals as a lightweight communication mechanism between processes and the kernel. When you press Ctrl+C in a terminal, the OS sends a SIGINT signal to your running program. Without signal handling, Ruby uses default behaviors that might not suit your application's needs. With proper handlers, you decide exactly what happens when signals arrive.
Signal handling becomes critical for long-running processes like web servers, background workers, and daemon applications. These programs need to finish current work, close database connections, and clean up resources before exiting. Understanding signals helps you build production-ready applications that behave reliably under real-world conditions.
In this article, we'll explore common Unix signals and their purposes, how to trap and handle signals in Ruby, and patterns for graceful shutdowns and signal coordination.
Let's dive in!
Prerequisites
You'll need Ruby 2.7 or later installed. Signal handling works across platforms, though some signals are Unix-specific:
Basic understanding of processes and how programs interact with the operating system helps, though we'll explain concepts as needed:
Setting up the environment
To demonstrate signal handling effectively, you'll create programs that respond to various signals. These examples show how Ruby intercepts signals and lets you define custom behavior instead of using default actions.
Signals operate asynchronously, arriving at unpredictable times during program execution. Ruby's signal handling API lets you register callbacks that execute when specific signals arrive. This mechanism works reliably across different signal types and supports both simple and complex handling scenarios.
Create your project directory:
Start with a basic signal handler to see how interception works:
This program runs indefinitely until interrupted. Without the trap handler, Ctrl+C would terminate immediately with Ruby's default behavior. The trap block intercepts SIGINT and executes custom code before exiting.
Run this and press Ctrl+C after a few seconds:
The trap handler caught the signal, printed a message, then exited cleanly. This demonstrates the basic pattern for all signal handling in Ruby.
Understanding common signals
Different signals serve distinct purposes in Unix-like systems. Some request program termination, others pause execution, and some carry custom application-specific meaning. Knowing which signals exist and what they typically mean helps you handle them appropriately.
SIGTERM requests graceful termination and is the standard way to ask a process to shut down. SIGINT comes from keyboard interrupts like Ctrl+C. SIGKILL forces immediate termination and cannot be caught. SIGHUP traditionally meant a terminal hangup but now often triggers configuration reloads. SIGUSR1 and SIGUSR2 are available for custom application logic.
Ruby provides access to most signals through string names or signal numbers. The Signal module includes methods for trapping signals and querying which signals the current platform supports. Not all signals exist on every platform, so defensive programming checks signal availability before trapping.
Create a signal reference program:
This shows all available signals and demonstrates handling multiple types. Different signals can share handlers or have distinct behaviors.
Run this in one terminal:
From another terminal, send signals to test different handlers:
The program responds to HUP but keeps running, then exits when TERM arrives. This shows how different signals trigger different behaviors.
Implementing graceful shutdowns
Production applications need to shut down cleanly when termination signals arrive. This means finishing active work, closing database connections, flushing logs, and releasing resources properly. Without graceful shutdown, you risk data loss, corrupted state, or resource leaks.
The pattern involves setting a shutdown flag when signals arrive, completing current operations, then exiting. Long-running tasks check the flag periodically and stop at safe points. This approach balances responsiveness with data integrity, ensuring work completes correctly before termination.
Ruby's trap handlers execute in a separate context with restrictions on what operations are safe. Complex shutdown logic should set flags that the main program checks rather than doing extensive work directly in the trap handler. This avoids threading issues and keeps signal handling predictable.
Create a worker with graceful shutdown:
The shutdown flag lets the current job finish before exiting. Signal handlers set the flag without interrupting work, ensuring clean completion.
Run this and interrupt after a few jobs:
The worker finished job 2 before exiting, demonstrating graceful shutdown in action.
Handling signals with timeouts
Sometimes shutdown needs a timeout to prevent hanging indefinitely. If work doesn't complete within a reasonable time, the program should force exit. This pattern combines graceful shutdown attempts with a fallback forced termination after a deadline.
Ruby's Timeout module provides deadline enforcement, though signals offer another approach. You can trap TERM for graceful shutdown and KILL as an emergency exit, or implement a two-stage shutdown where the first signal starts cleanup and a second forces termination.
The key is balancing patience for normal shutdown with responsiveness to repeated signals. Users expect the first interrupt to trigger cleanup and a second to force exit immediately. This matches common Unix tool behavior and user expectations.
Create a worker with timeout handling:
The first Ctrl+C triggers graceful shutdown, while a second forces immediate exit. This gives users control over shutdown urgency.
Run this and experiment with single and double interrupts:
Using signals for application control
Beyond shutdown, signals enable runtime control of applications. You can reload configuration, toggle debug modes, or trigger status reports without restarting programs. This makes signals valuable for production systems that need dynamic behavior changes.
SIGUSR1 and SIGUSR2 are designated for custom application logic since they have no standard meaning. Web servers often use SIGHUP to reload configuration and USR1 to reopen log files. Background workers might use signals to pause processing or dump internal state for debugging.
Signal-based control works well because it requires no special ports, files, or protocols. Any process with appropriate permissions can send signals using standard tools. This simplicity makes signals attractive for simple control interfaces on long-running processes.
Create an application with signal-based controls:
Different signals control debug output and pause state without stopping the program. This demonstrates practical signal-based runtime control.
Run the program:
Send signals to change behavior:
The program responds to signals by toggling modes, showing how signals enable runtime control.
Final thoughts
Signal handling lets Ruby programs respond safely to system events and user actions. The main idea is to know what each signal does, keep your handlers simple by using flags, and make sure your shutdown process finishes work safely before the program exits.
In production, graceful shutdowns help prevent data loss and resource leaks. Using signals gives you an easy way to control your app at runtime without adding complex systems. By following best practices for signal safety, your handlers will stay reliable in all situations. Check the Ruby Signal documentation and try out different signals to build stable, production-ready apps.