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:
include/asio/steady_timer.hpp– Defines thesteady_timertype alias andbasic_waitable_timerinterface.src/examples/cpp20/coroutines/timeout_watchdog.cpp– Demonstrates the watchdog coroutine pattern for inactivity timeouts.src/examples/cpp14/parallel_group/wait_for_one.cpp– Showssteady_timerused to limit parallel operation groups to specific durations.src/examples/cpp20/type_erasure/sleep.cpp– Minimal example usingsteady_timerfor coroutine delays.src/tests/unit/steady_timer.cpp– Unit tests confirming timer semantics and cancellation behavior.
Summary
steady_timerprovides a monotonic clock interface viabasic_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →