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

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 (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. 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 (lines 40-48). Each thread runs worker_loop_, which repeatedly calls process_next_msg_ as shown in 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 (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

#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, lines 31-55.

Using the Non-Blocking overrun_oldest Policy

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, lines 69-73 and src/details/mpmc_blocking_q.h, lines 40-47.

Flushing and Graceful Shutdown

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, lines 64-67). shutdown() ultimately destroys the thread pool, causing termination messages to be posted (see destructor, lines 45-49).

Key Source Files

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 (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 (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 (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, while producer submission and consumer dispatch are implemented in src/details/thread_pool-inl.h. Public factory functions are declared in include/spdlog/async.h, and the logger interface is found in include/spdlog/async_logger.h.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →