# How the spdlog Thread Pool Handles Log Messages: Inside Async Logging Architecture

> Explore spdlog's async logging architecture Understand how its thread pool enqueues and processes log messages efficiently for non-blocking application performance.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: internals
- Published: 2026-07-22

---

**spdlog's thread pool decouples log generation from I/O operations by enqueuing `async_msg` objects into a lock-free multi-producer-multi-consumer queue, allowing dedicated worker threads to format and dispatch messages to sinks asynchronously.**

The [gabime/spdlog](https://github.com/gabime/spdlog) library provides high-performance asynchronous logging through a dedicated thread pool that minimizes latency in the calling thread. Understanding how the spdlog thread pool handles log messages is essential for optimizing application performance and configuring overflow policies correctly. This article examines the internal implementation, from message construction to worker thread processing, based on the actual source code in the v1.x branch.

## Thread Pool Architecture and Components

The asynchronous logging subsystem centers on `spdlog::details::thread_pool`, defined in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h). This class manages a pool of `std::thread` objects and a lock-free `mpmc_blocking_queue` that serves as the communication channel between application threads and background workers.

When you create an async logger, you provide a shared pointer to this thread pool. The logger holds a weak reference to avoid circular dependencies, while the pool owns the queue and worker threads. The queue capacity and thread count are fixed at construction time to avoid dynamic memory allocation during hot paths.

## Message Lifecycle: From Enqueue to Sink

### Message Construction and Enqueueing

When an application calls a logging method like `logger->info()`, the async logger constructs an `async_msg` object. According to the definition in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h) (lines 25-70), this structure encapsulates:
- The `log_msg` containing the actual log content and metadata
- A pointer to the originating logger instance
- The message type enum (`log`, `flush`, or `terminate`)

The logger then invokes `thread_pool::post_log()` (or `post_flush()` for explicit flush operations). Inside [`include/spdlog/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool-inl.h) (lines 57-62), `post_log()` constructs the `async_msg` and delegates to the private `post_async_msg_()` method, which attempts to enqueue the message into the `mpmc_blocking_queue` at lines 81-88.

The queue's behavior depends on the configured **overflow policy**:
- **`block`** – Waits until space is available using condition variables
- **`overrun_oldest`** – Removes the oldest message from the queue to accommodate the new entry
- **`discard_new`** – Drops the incoming message immediately if the queue is full

### Worker Thread Processing Loop

During thread pool construction ([`thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/thread_pool-inl.h) lines 26-32), the constructor launches a configurable number of `std::thread` instances, each executing the `worker_loop_()` method. This loop runs indefinitely until a terminate signal is received.

The `worker_loop_()` method (lines 91-94) repeatedly calls `process_next_msg_()`, which blocks on the queue until an `async_msg` becomes available. When a message arrives, the worker determines its type and acts accordingly (lines 100-115):

- **Log messages**: Calls `worker_ptr->backend_sink_it_(incoming_async_msg)`, which formats the message and writes it to the underlying sink(s)
- **Flush messages**: Invokes `worker_ptr->backend_flush_()` to ensure all buffered output is synchronized
- **Terminate messages**: Sets the internal running flag to false, causing the worker thread to exit gracefully after dequeuing

### Graceful Shutdown Mechanics

When the thread pool destructor executes ([`thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/thread_pool-inl.h) lines 44-53), it posts a `terminate` message for each worker thread to ensure orderly shutdown. The destructor then joins all threads, guaranteeing that all previously queued messages are processed before the pool releases its resources.

## Key Source Files and Implementation Details

- **[`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h)**: Declares the `thread_pool` class, `async_msg` struct, and the `async_overflow_policy` enum
- **[`include/spdlog/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool-inl.h)**: Implements thread creation (`worker_loop_`), enqueue logic (`post_async_msg_`), and dispatch logic (`process_next_msg_`)
- **[`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h)**: Defines the async logger facade that forwards logging calls to the thread pool
- **[`src/async.cpp`](https://github.com/gabime/spdlog/blob/main/src/async.cpp)**: Provides the `init_thread_pool` convenience function for creating global thread pools with default settings

## Practical Usage Example

```cpp
#include "spdlog/spdlog.h"
#include "spdlog/async.h"
#include "spdlog/sinks/stdout_sinks.h"

// Create a thread pool with queue size of 8192 and 8 worker threads
auto tp = std::make_shared<spdlog::details::thread_pool>(8192, 8);

// Create async logger with block policy (wait if queue is full)
auto logger = std::make_shared<spdlog::async_logger>(
    "my_async_logger", 
    tp,
    spdlog::sinks::stdout_sink_mt::instance(),
    spdlog::async_overflow_policy::block
);

// This call returns immediately after enqueueing
logger->info("Non-blocking log call: {}", 42);

// Request flush - posts a flush message to the pool
logger->flush();

// The thread pool destructor automatically posts terminate messages
// and joins worker threads when the last logger reference is released

```

## Summary

- **spdlog thread pool** uses a lock-free `mpmc_blocking_queue` to pass `async_msg` objects between application threads and background workers, eliminating lock contention in the hot path
- **Message types** include `log` (standard logging), `flush` (buffer synchronization), and `terminate` (graceful shutdown signaling)
- **Three overflow policies** control queue-full behavior: `block` (wait for space), `overrun_oldest` (drop oldest), and `discard_new` (drop newest)
- **Worker threads** run in `worker_loop_()`, processing messages via `process_next_msg_()` and delegating actual I/O to `backend_sink_it_()` and `backend_flush_()`
- **Graceful shutdown** ensures all queued messages are processed before destruction via terminate messages and thread joining mechanisms

## Frequently Asked Questions

### How does spdlog prevent log messages from blocking the main application thread?

The spdlog thread pool operates on a producer-consumer pattern where the calling thread only executes `post_log()`, which constructs an `async_msg` and enqueues it into the lock-free `mpmc_blocking_queue`. This operation is typically wait-free or briefly blocking depending on the overflow policy, but never involves formatting or I/O operations. The actual disk write or console output happens later in the worker thread's `process_next_msg_()` function.

### What happens when the async queue fills up?

Behavior depends on the `async_overflow_policy` passed to the logger constructor. With `block`, the enqueueing thread waits until space is available. `overrun_oldest` removes the oldest message from the queue to accommodate the new one, while `discard_new` simply drops the incoming message. You can configure this per-logger when binding it to the thread pool.

### How many threads should I allocate to the spdlog thread pool?

The optimal thread count depends on your sink configuration. For single-file logging, one thread is usually sufficient because disk I/O is typically sequential. However, if you use multiple sinks or network-based sinks, allocating 2-4 threads can improve throughput by parallelizing formatting and output operations. The constructor `std::make_shared<spdlog::details::thread_pool>(queue_size, thread_count)` accepts any positive value, but creating more threads than sinks rarely improves performance.

### Is it safe to destroy the thread pool while loggers still reference it?

Yes, because async loggers hold weak pointers to the thread pool. When the pool destructor runs, it posts `terminate` messages to all worker threads and joins them, ensuring all pending `async_msg` objects are processed. Loggers that attempt to log after the pool is destroyed will safely fail to lock the weak pointer and drop the message rather than causing undefined behavior.