# ASIO co_spawn vs spawn: Choosing Between Stackful and Stackless Coroutines

> Decide between ASIO co_spawn and spawn for C++ coroutines. Learn which stackless or stackful option suits your project best for optimal performance and compatibility.

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

---

**Use `asio::co_spawn` for C++20 projects requiring zero-allocation coroutines with minimal memory overhead, and `asio::spawn` when working with pre-C++20 compilers or when you need true stackful semantics like deep recursion.**

The chriskohlhoff/asio library offers two distinct APIs for writing asynchronous code in a sequential style: `asio::spawn` for stackful coroutines and `asio::co_spawn` for stackless C++20 coroutines. While both achieve the same goal of simplifying asynchronous logic, they differ fundamentally in implementation, memory usage, and compiler requirements. This guide examines the **asio co_spawn vs spawn** decision based on the actual source code implementation.

## What Are the Two Coroutine Styles?

### Stackful Coroutines with asio::spawn

The `asio::spawn` function creates **stackful coroutines** using Boost.Context (formerly Boost.Coroutine). Each coroutine receives its own stack segment (approximately 64 KB by default) that is switched during suspension and resumption.

The core type is `basic_yield_context<Executor>` defined in [`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp). When you spawn a coroutine, Asio creates a `detail::spawned_thread_base`-derived object in [`include/asio/impl/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/spawn.hpp) that manages the stack allocation and cancellation state. The coroutine function receives a `yield_context` token that you pass to asynchronous operations.

### Stackless Coroutines with asio::co_spawn

The `asio::co_spawn` function leverages native **C++20 coroutines** using the `asio::awaitable<T>` type. Instead of allocating a separate stack, the compiler rewrites the coroutine function into a state machine.

The entry point resides in [`include/asio/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/co_spawn.hpp), which constructs a `detail::co_spawn_state` object (implemented in [`include/asio/impl/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/co_spawn.hpp)) to hold the awaitable and a work guard for the executor. Each `co_await` expression yields control back to the Asio executor without stack switching.

## Key Differences: Implementation and Performance

### Underlying Architecture

**`asio::spawn`** relies on stack switching via Boost.Context. When a coroutine suspends, the library saves the current CPU registers and switches to the scheduler's stack (or another coroutine). This happens through the `spawned_thread_base::resume()` method in [`include/asio/impl/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/spawn.hpp).

**`asio::co_spawn`** uses the compiler-generated coroutine promise. The function state is stored in a heap-allocated frame containing program counter positions and local variables. Control transfer happens through `co_spawn_dispatch` and `co_spawn_post` helpers in [`include/asio/impl/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/co_spawn.hpp), which integrate with Asio's `async_initiate` mechanism.

### Memory and Performance Characteristics

The **stackful** approach allocates a dedicated stack for each coroutine. This incurs higher memory usage (roughly 64 KB per coroutine) but provides true stack semantics where local variables persist across suspension points and deep recursion works naturally.

The **stackless** approach allocates only the coroutine frame required to store local variables and the resumption point. This results in significantly lower memory footprints and faster resumption times, though the compiler-generated state machine may increase binary size.

### Portability Requirements

**`asio::spawn`** works on any compiler supporting Boost.Context and does **not** require C++20. This makes it suitable for legacy codebases or environments with older toolchain requirements.

**`asio::co_spawn`** requires full C++20 coroutine support (`co_await`, `co_return`) and compliant standard library implementations. It is the preferred choice for modern C++ projects.

## Practical Code Examples

### Example 1: Stackful Coroutine with asio::spawn

When using `asio::spawn`, you pass the `yield_context` as a function argument and use it as a completion token for asynchronous operations.

```cpp
#include <asio.hpp>
#include <iostream>

void echo_session(asio::ip::tcp::socket socket, asio::yield_context yield)
{
  try
  {
    char data[1024];
    for (;;)
    {
      std::size_t n = socket.async_read_some(asio::buffer(data), yield);
      asio::async_write(socket, asio::buffer(data, n), yield);
    }
  }
  catch (std::exception& e)
  {
    std::cerr << "Session ended: " << e.what() << "\n";
  }
}

// Launch the coroutine
asio::io_context io;
asio::ip::tcp::acceptor acceptor(io, {asio::ip::tcp::v4(), 8080});
asio::spawn(io, [&](asio::yield_context yield) {
  auto socket = acceptor.async_accept(yield);
  echo_session(std::move(socket), yield);
});
io.run();

```

*Key files:* [`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp), [`include/asio/impl/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/spawn.hpp)

### Example 2: Stackless Coroutine with asio::co_spawn

With `asio::co_spawn`, the function returns `asio::awaitable<T>` and uses `co_await` to suspend execution.

```cpp
#include <asio.hpp>
#include <iostream>

asio::awaitable<void> echo_session(asio::ip::tcp::socket socket)
{
  try
  {
    char data[1024];
    for (;;)
    {
      std::size_t n = co_await socket.async_read_some(asio::buffer(data));
      co_await asio::async_write(socket, asio::buffer(data, n));
    }
  }
  catch (std::exception& e)
  {
    std::cerr << "Session ended: " << e.what() << "\n";
  }
}

// Launch the coroutine
asio::io_context io;
asio::ip::tcp::acceptor acceptor(io, {asio::ip::tcp::v4(), 8080});
asio::co_spawn(io, [&]() -> asio::awaitable<void> {
  auto socket = co_await acceptor.async_accept();
  co_await echo_session(std::move(socket));
}, asio::detached);
io.run();

```

*Key files:* [`include/asio/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/co_spawn.hpp), [`include/asio/impl/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/co_spawn.hpp)

### Example 3: Cancellation Handling Comparison

Both APIs support Asio's cancellation framework, but access differs slightly.

**Stackful (`spawn`):**

```cpp
asio::spawn(io, [&](asio::yield_context yield) {
  auto cancel_state = yield.get_cancellation_state();
  // Check cancel_state.cancelled() or rely on operation cancellation
});

```

**Stackless (`co_spawn`):**

```cpp
#include <asio/this_coro.hpp>

asio::awaitable<void> task()
{
  auto cancel_state = co_await asio::this_coro::cancellation_state;
  cancel_state.assign(asio::cancellation_type::all);
  // Cancellation slot is attached to the awaiting coroutine
}

```

*Reference:* [`include/asio/this_coro.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/this_coro.hpp)

## Exception Handling and Error Codes

**`asio::spawn`** provides two error handling patterns. You can pass an `error_code` to the yield token: `socket.async_read_some(buf, yield[ec])`, or let exceptions propagate naturally. The `try/catch` blocks behave exactly like normal function stacks because they operate on a real stack.

**`asio::co_spawn`** propagates exceptions from awaited operations unless you explicitly use the `error_code` overload: `co_await socket.async_read_some(buf, ec)`. While exception handling is standardized, remember that the coroutine is a compiler-generated state machine, not a traditional stack frame.

## When to Choose Which Style

Choose **`asio::spawn`** when:
- Your project must compile with pre-C++20 compilers
- You require true stackful semantics for deep recursion or complex call stacks
- You are maintaining existing Boost.Coroutine-based codebases
- You need predictable `try/catch` behavior identical to synchronous code

Choose **`asio::co_spawn`** when:
- You are targeting C++20 or later with modern compiler support
- Minimizing memory allocation is critical (no per-coroutine stack allocation)
- You want to blend Asio with other C++20 coroutine libraries (e.g., `std::generator`)
- You prefer the explicit `co_await` syntax and state machine model

## Summary

- **`asio::spawn`** implements **stackful coroutines** using Boost.Context with explicit stack allocation (~64 KB per coroutine), defined in [`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp) and [`include/asio/impl/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/spawn.hpp).
- **`asio::co_spawn`** implements **stackless coroutines** using C++20 `awaitable` types with compiler-generated state machines, defined in [`include/asio/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/co_spawn.hpp) and [`include/asio/impl/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/co_spawn.hpp).
- **Memory:** `spawn` allocates separate stacks; `co_spawn` allocates only the coroutine frame, resulting in lower memory usage.
- **Portability:** `spawn` works without C++20; `co_spawn` requires C++20 coroutine support.
- **Syntax:** `spawn` uses `yield_context` tokens; `co_spawn` uses `co_await` expressions.
- **Interoperability:** Both APIs integrate with Asio's `async_initiate` and support the cancellation framework via `this_coro::cancellation_state` or `yield.get_cancellation_state()`.

## Frequently Asked Questions

### Does asio::spawn require C++20?

No. `asio::spawn` relies on Boost.Context and works with pre-C++20 compilers. It only requires a compiler that can build the Boost.Coroutine2 or Boost.Context libraries. In contrast, `asio::co_spawn` requires a compiler with full C++20 coroutine support.

### Can I mix asio::spawn and asio::co_spawn in the same project?

Yes. Both APIs ultimately use Asio's `async_initiate` mechanism and can interoperate through the executor. You can spawn stackful coroutines from within stackless ones (and vice versa) by posting work to the same `io_context` or executor.

### Which coroutine style has better performance?

`asio::co_spawn` typically offers better performance and lower memory overhead because it avoids allocating separate stacks (~64 KB per coroutine) and uses compiler-optimized state machines. However, `asio::spawn` may perform better for highly recursive algorithms where stack allocation is actually beneficial.

### How do I get cancellation state in a C++20 coroutine?

Use `co_await asio::this_coro::cancellation_state` inside an `awaitable` function. This returns a `cancellation_state` object defined in [`include/asio/this_coro.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/this_coro.hpp), which allows you to check for cancellation requests or assign cancellation types (e.g., `cancellation_type::all`).