# Handling SIGINT and SIGTERM Gracefully with ASIO signal_set

> Learn to gracefully handle SIGINT and SIGTERM with ASIO signal_set. Master asynchronous signal monitoring and trigger clean shutdowns by stopping the io_context in your signal handler.

- Repository: [chriskohlhoff/asio](https://github.com/chriskohlhoff/asio)
- Tags: how-to-guide
- Published: 2026-07-11

---

**Use `asio::signal_set` to asynchronously monitor POSIX signals like SIGINT and SIGTERM, then trigger a clean shutdown by stopping the `io_context` from within the signal handler.**

The `asio::signal_set` class in the chriskohlhoff/asio repository provides a portable, asynchronous mechanism for handling process termination signals. Instead of blocking signal handlers or complex signal masking, you can register signals like **SIGINT** (Ctrl-C) and **SIGTERM** to gracefully shut down your application by stopping the `io_context` when these signals arrive.

## Core Architecture of signal_set

### basic_signal_set Implementation

The `asio::signal_set` name is actually a typedef for `asio::basic_signal_set<>`, defined in [`include/asio/signal_set.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/signal_set.hpp). The implementation resides in [`include/asio/basic_signal_set.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/basic_signal_set.hpp), where it inherits from `signal_set_base` and uses an internal `signal_set_service` to interact with the operating system kernel.

### Signal Registration and Management

You can register signals either through constructor overloads accepting up to three signals simultaneously, or dynamically using the `add()` and `remove()` methods. The constructor internally calls `add()` for each supplied signal. Both `add()` and `remove()` provide error-throwing and error-code overloads (e.g., `add(int, error_code&)`), forwarding to the service which registers the signal with the kernel while safely ignoring duplicate registrations.

### Queuing and Delivery Mechanism

If a signal arrives when no handler is waiting, the notification is queued and delivered on the next `async_wait`. Queued notifications are ordered by ascending signal number, ensuring deterministic delivery. The same signal can be registered on different `signal_set` objects, with each receiving a handler call—provided the signals were registered through ASIO rather than raw `signal()` or `sigaction()` system calls.

## Signal Masking and Thread Safety

On POSIX platforms, registered signals must be unblocked in at least one thread for the operating system to deliver them. This requirement is documented in the class description within [`basic_signal_set.hpp`](https://github.com/chriskohlhoff/asio/blob/main/basic_signal_set.hpp). The signal set uses the same executor as the surrounding `io_context`, ensuring thread-safe dispatch of signal handlers to the correct execution context.

## Implementing Graceful Shutdown

### Minimal Example

The pattern from [`src/examples/cpp11/http/server4/main.cpp`](https://github.com/chriskohlhoff/asio/blob/main/src/examples/cpp11/http/server4/main.cpp) (lines 38-49) demonstrates the canonical approach:

```cpp
#include <asio.hpp>
#include <signal.h>

int main()
{
    asio::io_context io;
    // Create signal_set bound to the io_context executor
    asio::signal_set signals(io);
    
    // Register termination signals
    signals.add(SIGINT);
    signals.add(SIGTERM);
#if defined(SIGQUIT)
    signals.add(SIGQUIT);
#endif

    // Async wait stops the io_context upon signal receipt
    signals.async_wait(
        [&io](const asio::error_code&, int)
        {
            io.stop();  // Graceful shutdown
        });

    io.run();
}

```

### Blocking with Futures

For synchronous-style handling, use `asio::use_future`:

```cpp
#include <asio.hpp>

int main()
{
    asio::io_context io;
    asio::signal_set signals(io, SIGINT, SIGTERM);

    // Block until signal arrives
    std::future<int> f = signals.async_wait(asio::use_future);
    int signum = f.get();  // Returns the signal number
    io.stop();
}

```

### Coroutine Integration (C++20)

For modern asynchronous code, use `asio::use_awaitable` as demonstrated in [`src/examples/cpp20/coroutines/echo_server.cpp`](https://github.com/chriskohlhoff/asio/blob/main/src/examples/cpp20/coroutines/echo_server.cpp):

```cpp
#include <asio.hpp>
#include <asio/awaitable.hpp>
#include <asio/use_awaitable.hpp>

asio::awaitable<void> monitor_signals(asio::io_context& ctx)
{
    asio::signal_set sigs(ctx.get_executor(), SIGINT, SIGTERM);
    co_await sigs.async_wait(asio::use_awaitable);  // Suspends until signal
    ctx.stop();                                     // Graceful termination
}

```

## Cancellation and Cleanup

The `cancel()` method forces pending `async_wait` operations to complete immediately with `asio::error::operation_aborted`. This does not modify the set of registered signals, allowing you to abort current waiters without unregistering the signals themselves.

## Summary

- `asio::signal_set` (typedef for `basic_signal_set<>`) provides asynchronous POSIX signal handling through [`include/asio/basic_signal_set.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/basic_signal_set.hpp)
- Register signals via constructor overloads or the `add()` method; remove with `remove()`
- Signals are queued and delivered in ascending numerical order if handlers are not immediately available
- Use `async_wait()` with lambdas, futures, or coroutines to handle signals without blocking threads
- Call `cancel()` to abort pending operations without unregistering signals
- Ensure signals are unblocked in at least one thread on POSIX systems for delivery to occur
- Reference implementation available in [`src/examples/cpp11/http/server4/main.cpp`](https://github.com/chriskohlhoff/asio/blob/main/src/examples/cpp11/http/server4/main.cpp)

## Frequently Asked Questions

### Can I register the same signal on multiple signal_set instances?

Yes. According to the chriskohlhoff/asio source code, the same signal can be registered on different `signal_set` objects, and each will receive a handler call. This works only for signals registered through ASIO, not through raw `signal()` or `sigaction()` system calls.

### What happens if a signal arrives before async_wait is called?

The notification is queued and delivered on the next `async_wait` invocation. Queued notifications are ordered by ascending signal number, ensuring deterministic delivery order when multiple signals are pending.

### Do I need to worry about signal masking when using signal_set?

Yes. On POSIX platforms, registered signals must be unblocked in at least one thread for the operating system to deliver them. The ASIO documentation in [`basic_signal_set.hpp`](https://github.com/chriskohlhoff/asio/blob/main/basic_signal_set.hpp) explicitly notes this requirement, though the library handles the internal registration once this prerequisite is met.

### How do I stop waiting for signals without destroying the signal_set?

Call `cancel()` on the `signal_set` instance. This forces any pending `async_wait` operations to complete immediately with `asio::error::operation_aborted`, while keeping the signal registrations active for future wait operations.