# Implementing Connection Timeouts with ASIO steady_timer: A Complete Guide

> Implement ASIO connection timeouts using steady_timer. Learn how to race I/O operations against deadline timers for reliable control over your network applications.

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

---

**ASIO's `steady_timer` is a monotonic clock-based waitable timer that enables reliable connection timeouts by racing asynchronous I/O operations against deadline timers on the same executor.**

The `chriskohlhoff/asio` library provides robust asynchronous networking primitives for C++, and implementing connection timeouts with ASIO `steady_timer` ensures your operations respect strict deadlines regardless of system time adjustments. This mechanism wraps `std::chrono::steady_clock` to create executor-aware watchdogs that integrate seamlessly with C++20 coroutines, callback-based handlers, and parallel operation compositions.

## ASIO steady_timer Architecture and Design

At its core, `steady_timer` is defined in [`include/asio/steady_timer.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/steady_timer.hpp) as a type alias for `basic_waitable_timer<chrono::steady_clock>`. This design binds the timer to a monotonic clock, guaranteeing that timeout durations remain unaffected by system time changes.

### Monotonic Clock Guarantees

Unlike system-clock-based timers, `steady_timer` uses `std::chrono::steady_clock` (or Boost.Chrono when C++11 is unavailable) to ensure zero skew from NTP adjustments or manual time changes. The class exposes three critical methods for timeout management: `expires_at()`, `expires_after()`, and `async_wait()`.

### Executor Integration

A `steady_timer` is constructed with an executor—such as `io_context.get_executor()` or `co_await this_coro::executor`—ensuring that timer callbacks execute on the same execution context as the protected I/O operation. This executor binding prevents race conditions between timeout handling and operation completion.

## Implementing Connection Timeouts with Coroutines

The canonical pattern for connection timeouts involves a watchdog coroutine that monitors a deadline while the primary operation executes. This approach appears in [`src/examples/cpp20/coroutines/timeout_watchdog.cpp`](https://github.com/chriskohlhoff/asio/blob/main/src/examples/cpp20/coroutines/timeout_watchdog.cpp).

### The Watchdog Pattern

The watchdog coroutine monitors a shared deadline and throws `std::system_error` when the timeout expires, while the echo coroutine resets the deadline after each successful read:

```cpp
// From src/examples/cpp20/coroutines/timeout_watchdog.cpp
awaitable<void> echo(tcp::socket& sock, time_point& deadline)
{
  char data[4196];
  for (;;)
  {
    // Refresh the deadline after each successful read.
    deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
    auto n = co_await sock.async_read_some(buffer(data), use_awaitable);
    co_await async_write(sock, buffer(data, n), use_awaitable);
  }
}

awaitable<void> watchdog(time_point& deadline)
{
  steady_timer timer(co_await this_coro::executor);
  auto now = std::chrono::steady_clock::now();
  while (deadline > now)
  {
    timer.expires_at(deadline);                // Set timer to the current deadline.
    co_await timer.async_wait(use_awaitable);  // Suspend until the timer fires.
    now = std::chrono::steady_clock::now();
  }
  // Deadline passed – signal timeout.
  throw std::system_error(std::make_error_code(std::errc::timed_out));
}

```

### Parallel Composition

To enforce the timeout, compose both coroutines using the `&&` operator so the first to complete determines the outcome:

```cpp
// The two coroutines are run in parallel; the first to finish wins.
co_await (echo(sock, deadline) && watchdog(deadline));

```

This pattern ensures that if the watchdog timer expires before the socket operation completes, the program receives a `timed_out` error code immediately.

## Callback-Based Timeout Implementation

For applications not using coroutines, implement the same race logic with callback handlers. The timer and socket share an executor to ensure ordered cancellation:

```cpp
template <typename Executor, typename ConnectHandler>
void async_connect_with_timeout(
    const tcp::endpoint& endpoint,
    const std::chrono::steady_clock::duration& timeout,
    Executor exec,
    ConnectHandler handler)
{
  // 1. Start async_connect.
  tcp::socket socket(exec);
  socket.async_connect(endpoint,
      [handler, &socket](const boost::system::error_code& ec) mutable {
          // Cancel the timer if the connect succeeded/failed first.
          socket.get_executor().context().cancel(timer);
          handler(ec);
      });

  // 2. Start a steady_timer that will expire after `timeout`.
  steady_timer timer(exec);
  timer.expires_after(timeout);
  timer.async_wait([handler, &socket](const boost::system::error_code& ec) mutable {
      if (!ec) {                     // Timer expired → timeout.
          socket.cancel();          // Cancel any pending connect.
          handler(make_error_code(std::errc::timed_out));
      }
  });
}

```

The timer and socket share the same executor, ensuring that cancellation and completion handlers execute sequentially without data races.

## Key Source Files and Examples

The `chriskohlhoff/asio` repository provides several reference implementations demonstrating `steady_timer` usage:

- **[`include/asio/steady_timer.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/steady_timer.hpp)** – Defines the `steady_timer` type alias and `basic_waitable_timer` interface.
- **[`src/examples/cpp20/coroutines/timeout_watchdog.cpp`](https://github.com/chriskohlhoff/asio/blob/main/src/examples/cpp20/coroutines/timeout_watchdog.cpp)** – Demonstrates the watchdog coroutine pattern for inactivity timeouts.
- **[`src/examples/cpp14/parallel_group/wait_for_one.cpp`](https://github.com/chriskohlhoff/asio/blob/main/src/examples/cpp14/parallel_group/wait_for_one.cpp)** – Shows `steady_timer` used to limit parallel operation groups to specific durations.
- **[`src/examples/cpp20/type_erasure/sleep.cpp`](https://github.com/chriskohlhoff/asio/blob/main/src/examples/cpp20/type_erasure/sleep.cpp)** – Minimal example using `steady_timer` for coroutine delays.
- **[`src/tests/unit/steady_timer.cpp`](https://github.com/chriskohlhoff/asio/blob/main/src/tests/unit/steady_timer.cpp)** – Unit tests confirming timer semantics and cancellation behavior.

## Summary

- **`steady_timer`** provides a monotonic clock interface via `basic_waitable_timer<chrono::steady_clock>`, ensuring timeout immunity to system time changes.
- The **watchdog pattern** uses a dedicated coroutine or callback to monitor deadlines while I/O operations execute, cancelling pending work upon expiration.
- **Executor binding** ensures that timer callbacks and I/O completion handlers execute on the same context, preventing race conditions.
- **Composition operators** like `&&` enable declarative timeout racing in C++20 coroutines, while callback implementations require explicit timer and socket management.

## Frequently Asked Questions

### What makes steady_timer different from system_timer in ASIO?

**`steady_timer`** uses `std::chrono::steady_clock`, which measures monotonic time and never decreases, making it ideal for measuring intervals and timeouts. **`system_timer`** uses `std::chrono::system_clock`, which tracks wall-clock time and can be affected by system time adjustments, making it suitable for calendar-based deadlines rather than connection timeouts.

### How do I cancel a pending socket operation when the timer expires?

When the `steady_timer` expires first, invoke `socket.cancel()` in the timer's completion handler. This posts a cancellation signal to the socket's pending operation, causing it to complete immediately with `operation_aborted`. According to the source code in [`timeout_watchdog.cpp`](https://github.com/chriskohlhoff/asio/blob/main/timeout_watchdog.cpp), this pattern ensures resources are released promptly upon timeout.

### Can I use steady_timer with C++20 coroutines?

Yes, `steady_timer` fully supports C++20 coroutines through the `use_awaitable` completion token. As shown in [`src/examples/cpp20/coroutines/timeout_watchdog.cpp`](https://github.com/chriskohlhoff/asio/blob/main/src/examples/cpp20/coroutines/timeout_watchdog.cpp), you can `co_await timer.async_wait(use_awaitable)` to suspend the coroutine until the deadline, enabling natural composition with other asynchronous operations using operators like `&&` and `||`.

### Should I use expires_at or expires_after for connection timeouts?

Use **`expires_after()`** when setting relative timeouts (e.g., "connect within 5 seconds") and **`expires_at()`** when tracking absolute deadlines (e.g., "complete by 14:00:00"). For connection timeouts, `expires_after(std::chrono::seconds(5))` is typically more convenient, while the watchdog pattern in [`timeout_watchdog.cpp`](https://github.com/chriskohlhoff/asio/blob/main/timeout_watchdog.cpp) uses `expires_at()` to monitor a continuously updating deadline variable.