Implementing Connection Timeouts with ASIO steady_timer: A Complete Guide

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 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.

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:

// 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:

// 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:

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:

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, 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, 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 uses expires_at() to monitor a continuously updating deadline variable.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →