# How ASIO Handles Multiple Concurrent Connections: io_context, Acceptors, and Thread Pools

> Discover how ASIO handles multiple concurrent connections using io_context, acceptors, and thread pools. Learn about efficient async I/O for high-performance networking.

- Repository: [chriskohlhoff/asio](https://github.com/chriskohlhoff/asio)
- Tags: deep-dive
- Published: 2026-07-12

---

**ASIO handles multiple concurrent connections by multiplexing asynchronous I/O operations through a single `io_context` that runs across a thread pool, using non-blocking `async_accept()` to spawn per-connection sockets that execute independent read/write handlers without explicit locking.**

The `chriskohlhoff/asio` library provides a scalable networking framework built on asynchronous I/O primitives. Understanding how ASIO handles multiple concurrent connections requires examining its core architecture—the `io_context` service, the socket acceptor pattern, and the executor model that enables thousands of simultaneous connections without blocking threads.

## The io_context Execution Model

In [`include/asio/io_context.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/io_context.hpp), the `io_context` class owns the underlying I/O services and acts as the central event scheduler. By calling `io_context.run()` on multiple threads, the same context can process many asynchronous operations in parallel. This allows the program to handle thousands of connections concurrently without creating a thread per socket.

## Non-Blocking Connection Acceptance

The `basic_socket_acceptor` class, defined in [`include/asio/basic_socket_acceptor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/basic_socket_acceptor.hpp), manages the listening endpoint. Its `async_accept()` method returns immediately, handing the newly accepted socket to a user-supplied handler while the acceptor remains ready to accept the next connection. This pattern ensures that the listening socket never blocks the thread pool, allowing rapid acceptance of multiple simultaneous incoming connections.

## Per-Connection Asynchronous Operations

Each accepted socket runs its own set of asynchronous read/write operations. Because these operations are posted to the same `io_context`, they can be interleaved across threads without explicit locking. When a handler finishes, the `io_context` picks up the next ready operation from any connection, keeping the CPU busy across the entire connection pool.

## Thread Safety and Strand Serialization

Concurrency safety is guaranteed by the underlying operating-system primitives (e.g., epoll, kqueue, IOCP). ASIO's wrappers translate these primitives into completion handlers invoked on any thread running the `io_context`. When ordered access to shared data is required, a **strand** (`asio::strand`), implemented in [`include/asio/strand.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/strand.hpp), can be attached to an executor. This serializes handlers for a particular connection while allowing other connections to proceed concurrently.

## Example: Echo Server with Coroutines

The echo server example in [`src/examples/cpp20/coroutines/echo_server.cpp`](https://github.com/chriskohlhoff/asio/blob/main/src/examples/cpp20/coroutines/echo_server.cpp) demonstrates the complete pattern. The listener coroutine repeatedly `co_await`s `acceptor.async_accept()` and then `co_spawn`s a new coroutine for each accepted socket. All coroutines share the same `io_context`, which can be run on multiple threads to increase throughput.

```cpp
// Set up a thread-pooled io_context.
asio::io_context ctx{4};          // 4 threads will run ctx.run()

// Create an acceptor listening on port 55555.
asio::ip::tcp::acceptor acc(
    ctx.get_executor(),
    asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 55555));

// Accept connections and launch a coroutine for each.
auto acceptor_coro = [&]() -> asio::awaitable<void> {
  for (;;) {
    // async_accept returns a socket when a client connects.
    asio::ip::tcp::socket sock = co_await acc.async_accept(asio::use_awaitable);
    // Handle the connection in its own coroutine.
    asio::co_spawn(ctx, echo(std::move(sock)), asio::detached);
  }
};
asio::co_spawn(ctx, acceptor_coro(), asio::detached);

// Run the io_context on a pool of threads.
std::vector<std::thread> pool;
for (int i = 0; i < 4; ++i)
  pool.emplace_back([&]{ ctx.run(); });
for (auto& t : pool) t.join();

```

The per-connection echo coroutine handles reads and writes independently:

```cpp
// Simple per-connection echo coroutine.
asio::awaitable<void> echo(asio::ip::tcp::socket sock) {
  char data[1024];
  for (;;) {
    std::size_t n = co_await sock.async_read_some(
        asio::buffer(data), asio::use_awaitable);
    co_await asio::async_write(sock, asio::buffer(data, n),
                               asio::use_awaitable);
  }
}

```

## Summary

- **Single I/O Service**: The `io_context` in [`include/asio/io_context.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/io_context.hpp) multiplexes many sockets across a thread pool.
- **Non-Blocking Accept**: The `basic_socket_acceptor` uses `async_accept()` to handle incoming connections without blocking, as defined in [`include/asio/basic_socket_acceptor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/basic_socket_acceptor.hpp).
- **Per-Connection Handlers**: Each socket operates independent asynchronous read/write operations that are scheduled across the thread pool.
- **Optional Serialization**: `asio::strand` from [`include/asio/strand.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/strand.hpp) provides ordered execution when handlers must access shared data.

## Frequently Asked Questions

### Does ASIO create a new thread for every connection?

No, ASIO does not create a new thread for every connection. Instead, a fixed pool of threads calls `io_context.run()`, and the `io_context` schedules completion handlers for all connections across these threads. This approach prevents resource exhaustion and supports thousands of concurrent connections with a limited number of threads.

### What is the role of asio::strand when handling concurrent connections?

`asio::strand` is an executor wrapper that ensures handlers posted to it execute serially, even if invoked from different threads. When a specific connection requires ordered access to shared state, attaching a strand guarantees that its handlers do not run concurrently, eliminating the need for explicit mutexes while allowing other connections to proceed in parallel.

### How does async_accept handle multiple simultaneous incoming connections?

The `async_accept` function in `basic_socket_acceptor` returns immediately after initiating the accept operation, registering a completion handler that runs when a connection arrives. Because the acceptor immediately becomes ready to accept the next connection, multiple incoming connections can queue up and be processed sequentially by the `io_context` without blocking the thread pool.

### Can ASIO handle concurrent connections on a single thread?

Yes, ASIO can handle multiple concurrent connections on a single thread by running `io_context.run()` on just one thread. In this mode, the `io_context` still multiplexes all socket operations, though the throughput is limited to that single thread's capacity. For production servers, running the `io_context` across multiple threads is recommended to maximize throughput.