# Producer-Consumer Pattern in spdlog Async Logging: Architecture and Implementation

> Understand the producer consumer pattern in spdlog async logging. Learn how spdlog decouples app threads from worker threads using a lock-protected circular buffer for efficient logging.

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

---

**spdlog implements the producer-consumer pattern for asynchronous logging by decoupling application threads that produce `log_msg` records via `async_logger` from a pool of worker threads that consume `async_msg` structures from a lock-protected circular buffer inside `details::mpmc_blocking_queue`.**

The `gabime/spdlog` library uses a classic producer-consumer design to ensure that slow disk I/O never blocks fast application threads. In this architecture, an `async_logger` instance acts as the producer, packaging log events and handing them off to a global `details::thread_pool`. A fixed-size pool of worker threads then consumes those messages in the background, applying the configured sinks independently of the caller. Understanding this **producer-consumer pattern in spdlog async logging** is essential for tuning queue depth, overflow policies, and thread counts.

## Core Components of the spdlog Producer-Consumer Pattern

### Producer: async_logger and thread_pool::post_log

When the application calls `logger->info(...)` on an asynchronous logger, the `async_logger::log` method constructs a `details::log_msg` and forwards it to the global thread pool by invoking `details::thread_pool::post_log`. As implemented in [`src/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/src/details/thread_pool-inl.h) (lines 57-62), this function wraps the payload in an `async_msg` that stores a shared pointer to the originating logger and the log content, then submits it to the queue.

### Queue: mpmc_blocking_q

The shared channel is a `details::mpmc_blocking_queue<async_msg>` defined in [`src/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/src/details/mpmc_blocking_q.h). It is a lock-protected circular buffer that supports both blocking and non-blocking insertion. Producers use `enqueue` (lines 30-38) when the overflow policy is set to block, or `enqueue_nowait` (lines 40-47) when the policy is `overrun_oldest`, an option that drops the oldest entry to make room. The queue also tracks over-run and discard counters for diagnostics.

### Consumer: Worker Threads and worker_loop_

The consumer side is handled by worker threads spawned during thread pool initialization via `init_thread_pool` or the default factory in [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) (lines 40-48). Each thread runs `worker_loop_`, which repeatedly calls `process_next_msg_` as shown in [`src/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/src/details/thread_pool-inl.h) (lines 91-122). This consumer routine dequeues the next `async_msg`, inspects its `msg_type`, and dispatches it to `backend_sink_it_` for log messages, `backend_flush_` for flushes, or exits when a `terminate` message is received.

## End-to-End Message Flow in spdlog Async Logging

The producer-consumer pipeline follows a strict lifecycle from logger creation to shutdown:

1. **Logger creation.** `async_factory_impl::create` in [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) (lines 31-55) builds an `async_logger` and ensures a global `thread_pool` exists.
2. **Logging call.** The public logger API constructs a `log_msg` and forwards it through `thread_pool::post_log`.
3. **Enqueue.** The message is converted into an `async_msg` and placed onto the MPMC queue by a producer thread.
4. **Consume.** Worker threads continuously dequeue messages inside `process_next_msg_` and process them without blocking the original caller.
5. **Shutdown.** On thread pool destruction, a `terminate` message is posted to each worker, causing `process_next_msg_` to return `false` and the thread to join cleanly.

This design cleanly separates the fast producer path from the slow I/O-bound consumer path, ensuring that application threads only stall if the queue is full and the selected overflow policy is `block`.

## Practical Code Examples

### Creating an Asynchronous Logger with Default Blocking Policy

```cpp
#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>

int main() {
    // Initialise a global thread pool (optional – spdlog creates one automatically)
    spdlog::init_thread_pool(8192, 2);      // queue size, number of worker threads

    // Create an async logger that writes to a file
    auto logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>(
        "async_file", "logs.txt");

    logger->info("Hello from the producer thread!");
}

```

*The call to `create_async` ultimately invokes `async_factory_impl::create`, which ensures a global thread pool exists and registers the new logger.* Relevant source: [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h), lines 31-55.

### Using the Non-Blocking overrun_oldest Policy

```cpp
auto nb_logger = spdlog::create_async_nb<spdlog::sinks::stdout_sink_mt>("nb_console");
nb_logger->warn("This uses the overrun-oldest policy.");

```

*Here `create_async_nb` selects `async_overflow_policy::overrun_oldest`. The queue’s `enqueue_nowait` method is used, which discards the oldest entry when the queue is full.* Relevant source: [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h), lines 69-73 and [`src/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/src/details/mpmc_blocking_q.h), lines 40-47.

### Flushing and Graceful Shutdown

```cpp
logger->flush();                 // posts a flush message to the queue
spdlog::shutdown();             // posts terminate messages, joins worker threads

```

*Flushing posts an `async_msg_type::flush` to the queue (see `post_flush` in [`src/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/src/details/thread_pool-inl.h), lines 64-67). `shutdown()` ultimately destroys the thread pool, causing termination messages to be posted (see destructor, lines 45-49).*

## Key Source Files

- **[`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h)** — Public async API, thread-pool creation, and logger factories.
- **[`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h)** — Declaration of `thread_pool`, queue type, and public methods.
- **[`src/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/src/details/thread_pool-inl.h)** — Implementation of the thread pool, worker loop, and message dispatch.
- **[`src/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/src/details/mpmc_blocking_q.h)** — Multi-producer-multi-consumer blocking queue implementation.
- **[`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h)** — Async logger interface and the `log` method that forwards to the pool.

## Summary

- The **producer-consumer pattern in spdlog async logging** relies on `async_logger` as the producer, `details::mpmc_blocking_queue` as the bounded buffer, and `details::thread_pool` worker threads as the consumers.
- `thread_pool::post_log` in [`src/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/src/details/thread_pool-inl.h) (lines 57-62) enqueues `async_msg` objects, while `worker_loop_` and `process_next_msg_` (lines 91-122) dequeue and dispatch them.
- The MPMC queue supports both **blocking** (`enqueue`) and **non-blocking** (`enqueue_nowait`) insertion strategies, selectable via overflow policy.
- Shutdown is coordinated by posting `terminate` messages to each worker, guaranteeing clean thread joining without leaking log records.

## Frequently Asked Questions

### What is the role of async_logger in the spdlog producer-consumer pattern?

`async_logger` acts as the producer. When the application invokes a logging method, the logger builds a `details::log_msg` and immediately hands it off to the global `details::thread_pool` through `post_log`. This keeps the calling thread free from sink I/O.

### How does spdlog handle a full queue in async logging?

Behavior is controlled by the `async_overflow_policy`. The default `block` policy causes producers to wait using the queue’s `enqueue` method. The `overrun_oldest` policy uses `enqueue_nowait` in [`src/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/src/details/mpmc_blocking_q.h) (lines 40-47) to overwrite the oldest message when capacity is reached.

### What happens during shutdown of the spdlog thread pool?

When the thread pool is destroyed or `spdlog::shutdown()` is called, the destructor posts an `async_msg_type::terminate` message to every worker thread in [`src/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/src/details/thread_pool-inl.h) (lines 45-49). Each consumer thread then exits its `worker_loop_` and joins cleanly.

### Which source files implement the spdlog async producer-consumer queue?

The queue implementation lives in [`src/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/src/details/mpmc_blocking_q.h), while producer submission and consumer dispatch are implemented in [`src/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/src/details/thread_pool-inl.h). Public factory functions are declared in [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h), and the logger interface is found in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h).