Implementing Reconnection Logic with ASIO async_connect: A Complete Guide

Use asio::async_connect with an asio::steady_timer to schedule retries, capturing your socket and timer in std::shared_ptr to maintain their lifetimes across asynchronous operations, and re-invoke async_connect in the timer's completion handler after detecting connection failures.

ASIO's async_connect function provides a robust foundation for implementing reconnection logic in network applications. This guide examines the implementation details in the chriskohlhoff/asio repository to show you how to build reliable retry mechanisms using composed asynchronous operations.

Understanding the async_connect Composed Operation

asio::async_connect is a composed asynchronous operation that attempts to open a socket by trying each endpoint in a sequence until one succeeds. According to the ASIO source code in include/asio/connect.hpp, the library provides two generic initiating function overloads:

  • Range-based overload (lines 45-52): Works with any container providing begin()/end(), such as tcp::resolver::results_type. This delegates to detail::initiate_async_range_connect.
  • Iterator-based overload (lines 90-98): Works directly with arbitrary iterators, delegating to detail::initiate_async_iterator_connect.

Both overloads forward the actual work to include/asio/impl/connect.hpp, where the core logic resides in two operation classes:

  • detail::range_connect_op: Drives range-based connections, storing the socket, endpoint range, current index, and user handler.
  • detail::iterator_connect_op: Implements the same logic for arbitrary iterator pairs.

These classes inherit from base_from_cancellation_state to honor ASIO's cancellation types and base_from_connect_condition to optimize away overhead when using the default condition.

How Reconnection Logic Works with async_connect

When async_connect completes, your handler receives an asio::error_code and the connected endpoint (or a default-constructed endpoint on failure). Because async_connect already loops through the endpoint list internally, you only need to implement reconnection when the entire sequence fails or when handling transient network errors.

The standard pattern involves three steps:

  1. Detect failure in the completion handler by checking error != asio::error::operation_aborted.
  2. Schedule a retry using an asio::steady_timer to prevent tight loops and reduce server load.
  3. Re-invoke async_connect with the same endpoint range (or a refreshed one after a new DNS resolve).

Basic Reconnection Implementation

The following example demonstrates a fixed-interval retry loop using std::shared_ptr to manage object lifetimes across asynchronous operations:

void reconnect(asio::io_context& ctx,
               const tcp::resolver::results_type& eps,
               std::size_t max_attempts = 0)
{
    auto sock = std::make_shared<tcp::socket>(ctx);
    auto tmr  = std::make_shared<asio::steady_timer>(ctx);
    std::size_t attempts = 0;

    std::function<void()> try_connect = [=, &try_connect]() mutable {
        if (max_attempts && attempts >= max_attempts) {
            std::cerr << "Giving up after " << attempts << " attempts.\n";
            return;
        }
        ++attempts;

        asio::async_connect(*sock, eps,
            [=](const asio::error_code& ec, const tcp::endpoint& ep) mutable {
                if (!ec) {
                    std::cout << "Connected to " << ep << "\n";
                    return;
                }

                std::cerr << "Attempt " << attempts
                          << " failed: " << ec.message() << "\n";

                tmr->expires_after(std::chrono::seconds(2));
                tmr->async_wait([=](const asio::error_code&) { try_connect(); });
            });
    };

    try_connect();          // First attempt.
}

Managing Object Lifetimes

Notice that both the socket and timer are captured by std::shared_ptr. This ensures they remain alive during the entire reconnection sequence, even when the initial reconnect function scope exits. The std::function wrapper allows the lambda to recursively invoke itself.

Advanced Reconnection Patterns

Exponential Back-off

To implement exponential back-off, multiply the delay after each failure, capping at a maximum value:

void reconnect_with_backoff(asio::io_context& ctx,
                            const tcp::resolver::results_type& eps,
                            asio::cancellation_signal& cancel_signal)
{
    auto sock = std::make_shared<tcp::socket>(ctx);
    auto tmr  = std::make_shared<asio::steady_timer>(ctx);
    std::chrono::seconds delay{1};

    std::function<void()> attempt = [=, &attempt]() mutable {
        if (cancel_signal.stop_requested())
            return;   // Early exit if cancelled.

        asio::async_connect(*sock, eps,
            [=](const asio::error_code& ec, const tcp::endpoint& ep) mutable {
                if (!ec) {
                    std::cout << "Connected to " << ep << "\n";
                    return;
                }

                std::cerr << "Connect failed: " << ec.message()
                          << " – retry in " << delay.count() << "s\n";

                tmr->expires_after(delay);
                tmr->async_wait([=](const asio::error_code&) {
                    // Double the delay, but cap at 30 seconds.
                    delay = std::min(delay * 2, std::chrono::seconds{30});
                    attempt();
                });
            });
    };

    // Bind the cancellation signal to the socket's async operations.
    sock->cancel(cancel_signal);
    attempt();
}

Cancellation Support

The range_connect_op and iterator_connect_op classes in include/asio/impl/connect.hpp inherit from base_from_cancellation_state, meaning they respect cancellation_type::partial and cancellation_type::terminal. You can use asio::cancellation_signal to gracefully abort reconnection attempts.

Dynamic Endpoint Refresh

For long-running applications, resolve fresh endpoints before retrying instead of reusing the original endpoints object. Inside the timer's completion handler, call resolver.async_resolve again and pass the new results to async_connect.

Logging with Connect Conditions

Provide a custom connect condition to log each endpoint attempt before connection:

struct logging_condition {
    bool operator()(const asio::error_code&, const tcp::endpoint& ep) const {
        std::cout << "Trying " << ep << "\n";
        return true;   // Continue with this endpoint.
    }
};

asio::async_connect(socket, endpoints, logging_condition(),
    [](const asio::error_code& ec, const tcp::endpoint& ep) { /* … */ });

The condition is cheap because the library uses empty-base optimization in base_from_connect_condition to fold it away when using the default condition.

Key Source Files in the ASIO Repository

File Description
include/asio/connect.hpp Public API for connect and async_connect (both range- and iterator-based overloads). Contains documentation, signatures, and initiation helpers.
include/asio/impl/connect.hpp Core implementation defining detail::range_connect_op, detail::iterator_connect_op, and the internal logic for iterating through endpoints.
include/asio/basic_socket.hpp Declaration of basic_socket and its member async_connect that the composed operation ultimately calls.
include/asio/steady_timer.hpp Timer class used for back-off and retry scheduling.
include/asio/cancellation_signal.hpp Provides cancellation support that can be bound to sockets or timers.

Summary

  • asio::async_connect is a composed operation that automatically iterates through endpoint sequences, simplifying reconnection logic.
  • The implementation uses detail::range_connect_op and detail::iterator_connect_op in include/asio/impl/connect.hpp to manage the connection loop.
  • Implement reconnection by scheduling an asio::steady_timer in the error handler and re-invoking async_connect after the delay.
  • Use std::shared_ptr to manage socket and timer lifetimes across retry attempts.
  • Support advanced features like exponential back-off, cancellation via cancellation_signal, and dynamic endpoint refresh by combining timers with resolver operations.

Frequently Asked Questions

How does async_connect handle multiple endpoints?

async_connect attempts each endpoint in the provided sequence until one succeeds or all fail. The internal range_connect_op class stores the current iterator and issues socket_.async_connect(*iter, ...) for each endpoint, automatically advancing to the next on failure. This happens inside the composed operation, so your handler only receives the final result after all endpoints have been exhausted.

Can I cancel an ongoing reconnection attempt?

Yes. The operation classes inherit from base_from_cancellation_state and respect ASIO's cancellation types. Create an asio::cancellation_signal, bind it to your socket using sock->cancel(cancel_signal), and call cancel_signal.emit() to abort the current async_connect or timer wait. This is useful for graceful shutdowns when your application needs to exit.

What is the difference between range-based and iterator-based async_connect?

The range-based overload accepts any container with begin()/end() (like the result of tcp::resolver::resolve), while the iterator-based overload accepts two iterators defining a sequence. Both eventually call the same underlying implementation in include/asio/impl/connect.hpp, but the range-based version handles index tracking automatically, whereas the iterator-based version works with arbitrary forward iterators.

Should I reuse endpoint results or resolve fresh addresses on retry?

For TCP connections, reusing the original resolver::results_type is efficient for immediate retries. However, for long-lived services or when dealing with dynamic DNS, resolve fresh endpoints before retrying by calling resolver.async_resolve inside your timer handler. This ensures you connect to currently available hosts if the IP addresses have changed since the last attempt.

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 →