# How spdlog's Thread Pool Enables Asynchronous Logging: Architecture Deep Dive

> Explore spdlog's thread pool for asynchronous logging. Learn how it offloads sink I/O to dedicated threads, enabling non-blocking log message queuing for faster application performance.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: deep-dive
- Published: 2026-08-06

---

**spdlog implements asynchronous logging by off-loading sink I/O operations to a dedicated thread pool, allowing application threads to queue log messages and return immediately without blocking on disk or network writes.**

The [gabime/spdlog](https://github.com/gabime/spdlog) library is a fast C++ logging library whose async logging support is built on a custom `thread_pool` class. This architecture separates the latency-critical logging API from the potentially slow sink operations, achieving high throughput through lock-free queueing and configurable worker threads.

## Core Components of the Async Logging Pipeline

The spdlog thread pool implementation consists of four tightly integrated components:

| Component | Role | Key Source File |
|-----------|------|-----------------|
| **`async_logger`** | Public API that queues log messages and flush requests | [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) |
| **`thread_pool`** | Manages worker threads and the bounded queue | [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h) |
| **`mpmc_blocking_queue`** | Lock-free ring buffer for `async_msg` objects | [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h) |
| **`async_msg`** | Wrapper for log messages, flush commands, or termination signals | [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h) |

### How Messages Flow Through the System

When your application calls `logger->info()`, the data flows through three distinct stages:

1. **Capture** — `async_logger` packages the message without I/O
2. **Queue** — `thread_pool` inserts the message into the `mpmc_blocking_queue`
3. **Process** — Worker threads dequeue and execute sink writes

## Queueing Log Calls: The Non-Blocking Frontend

The `async_logger` class overrides `sink_it_()` to intercept all log calls. Instead of writing directly to sinks, it delegates to the thread pool:

```cpp
pool_ptr->post_log(shared_from_this(), msg, overflow_policy_);

```

In [`include/spdlog/async_logger-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger-inl.h), this path creates an `async_msg` of type `log` containing:

- A `std::shared_ptr` to the originating logger
- The complete `log_msg` payload (timestamp, level, message, source location)
- The message type discriminator

The `thread_pool::post_async_msg_()` method then enqueues this message according to the **overflow policy** (`block`, `overrun_oldest`, or `discard_new`). The application thread returns immediately—no disk flush, no mutex contention on sink output, no network latency.

This implementation appears in [`include/spdlog/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool-inl.h) at lines 57–63.

## Worker Thread Architecture

The `thread_pool` constructor spawns **N** worker threads (default 1) that execute a continuous processing loop:

```cpp
while (process_next_msg_()) { /* loop */ }

```

Each thread in [`include/spdlog/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool-inl.h) (lines 91–115) performs:

1. **Dequeue** — Block on `mpmc_blocking_queue` until a message arrives
2. **Dispatch** — Switch on `async_msg::msg_type`:
   - `log` → `worker_ptr->backend_sink_it_(msg)`
   - `flush` → `worker_ptr->backend_flush_()`
   - `terminate` → Return `false` to exit loop

The `backend_sink_it_()` function iterates over all attached sinks, checks their individual log levels, and forwards formatted messages. This is the **only** code path where actual I/O occurs—file writes, console output, or network operations all happen inside these worker threads.

## Backend Execution and Sink Interaction

The backend functions defined in [`include/spdlog/async_logger-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger-inl.h) (lines 62–80) transform the queued `async_msg` back into sink operations:

```cpp
// Conceptual flow inside backend_sink_it_
for (auto& sink : sinks_) {
    if (sink->should_log(msg.level)) {
        sink->log(msg);  // Actual I/O happens here
    }
}

```

This design achieves **thread safety without per-log mutexes**: the MPMC queue handles synchronization, and individual sinks operate exclusively within a single worker thread context. Sinks that require internal locking (like `stdout_color_sink_mt`) remain correct, while lock-free sinks achieve maximum performance.

## Graceful Shutdown Mechanics

When the `thread_pool` destructor runs, it guarantees no log messages are lost:

```cpp
// From thread_pool-inl.h lines 43-53
for (size_t i = 0; i < threads_.size(); ++i) {
    post_async_msg_(async_msg{async_msg_type::terminate}, 
                    async_overflow_policy::block);
}
for (auto& t : threads_) {
    t.join();
}

```

Each worker receives a `terminate` message, processes any remaining queued messages, then exits cleanly. The `join()` ensures the destructor blocks until all pending I/O completes.

## Configurable Overflow Policies

The `async_overflow_policy` enum controls queue-full behavior:

| Policy | Behavior | Use Case |
|--------|----------|----------|
| `block` | Caller blocks until queue space available | Reliability-critical applications |
| `overrun_oldest` | Replace oldest message with newest | Latency-sensitive, can tolerate drops |
| `discard_new` | Silently drop newest message | High-throughput, loss-tolerant logging |

Policy enforcement occurs in `thread_pool::post_async_msg_()` at lines 81–88 of [`include/spdlog/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool-inl.h), using the `mpmc_blocking_queue` methods `enqueue()`, `enqueue_nowait()`, and `enqueue_if_have_room()`.

## Complete Working Example

```cpp
#include <spdlog/spdlog.h>
#include <spdlog/async_logger.h>
#include <spdlog/details/thread_pool.h>
#include <spdlog/sinks/basic_file_sink.h>

int main() {
    // Create thread pool: 8192-item queue, 2 worker threads
    auto tp = std::make_shared<spdlog::details::thread_pool>(
        8192,  // queue_max_items
        2      // worker_threads
    );

    // Build file sink
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
        "async_log.txt"
    );

    // Create async logger with block-on-full policy
    auto logger = std::make_shared<spdlog::async_logger>(
        "file_async",
        file_sink,
        tp,
        spdlog::async_overflow_policy::block
    );

    // Log from main thread — returns immediately, I/O happens in workers
    logger->info("Processing request id={}", 42);
    logger->warn("High latency detected: {}ms", 150);

    // Async flush — posts flush command to queue
    logger->flush();

    // Graceful shutdown: destroy pool, wait for workers
    tp.reset();  // or let it go out of scope
    return 0;
}

```

## Key Source Files Reference

| File | Purpose |
|------|---------|
| [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h) | `thread_pool` class declaration, `async_msg` struct, queue types |
| [`include/spdlog/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool-inl.h) | Worker thread creation, message posting, dispatch loop, shutdown |
| [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) | Public `async_logger` API and overflow policy definitions |
| [`include/spdlog/async_logger-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger-inl.h) | Backend sink iteration and flush implementation |
| [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h) | Lock-free bounded MPMC queue implementation |

## Summary

- **spdlog's thread pool** decouples logging API calls from sink I/O through a bounded lock-free queue
- **`async_logger`** packages messages without blocking; **`thread_pool`** workers execute all sink operations
- **Worker threads** consume `async_msg` objects and invoke `backend_sink_it_()` or `backend_flush_()` exclusively
- **Overflow policies** (`block`, `overrun_oldest`, `discard_new`) let applications trade reliability for latency
- **Graceful shutdown** guarantees all queued messages are processed before destruction via terminate messages and `join()`

## Frequently Asked Questions

### How does spdlog prevent log message loss during high load?

spdlog's thread pool uses a **bounded queue** with configurable overflow policy. The default `block` policy causes the calling thread to wait until queue space is available, ensuring no messages are dropped. For loss-tolerant scenarios, `overrun_oldest` or `discard_new` policies apply backpressure by removing messages rather than blocking producers.

### What is the optimal number of worker threads for spdlog's async logging?

For **single-file sinks**, one worker thread is optimal—multiple threads would contend for the same file lock. For **multiple distinct sinks** or **network sinks**, match thread count to the number of independent I/O channels. The constructor default of 1 thread suits most workloads; profile your specific sink mix to determine if additional threads improve throughput without increasing contention.

### Can I use the same thread pool for multiple async loggers?

Yes. A single `thread_pool` instance can service multiple `async_logger` objects, which is the recommended pattern to control total thread count. The pool's MPMC queue handles concurrent enqueues from any number of loggers, and worker threads dispatch to the appropriate logger's backend via the `shared_ptr` stored in each `async_msg`.

### How does spdlog's async performance compare to synchronous logging?

For **fast sinks** (memory buffers, `null_sink`), sync logging avoids queue overhead and is faster. For **slow sinks** (rotating files, network endpoints, remote syslog), async logging prevents application threads from stalling on I/O latency. The **lock-free MPMC queue** minimizes enqueue overhead to approximately 50-100 nanoseconds on modern hardware, making async preferable when sink latency exceeds 1 microsecond.