How Async Logging Works in spdlog: Thread Pools and Overflow Policies Explained

spdlog implements asynchronous logging through a global thread pool that decouples log production from consumption, using a lock-free queue with configurable overflow policies (block, overrun_oldest, discard_new) to handle backpressure.

spdlog provides high-performance async logging through a sophisticated thread pool architecture that minimizes contention between application threads and I/O operations. The gabime/spdlog library achieves this by enqueuing log messages to dedicated worker threads that handle sink writes, while overflow policies determine system behavior when the queue reaches capacity.

Global Thread Pool Architecture

The async system centers on a singleton thread pool created lazily when the first async logger is instantiated. In include/spdlog/async.h (lines 75-94), async_factory_impl::create checks details::registry::instance().get_tp() and constructs a default pool with 8192 queue slots and one worker thread if none exists.

Users can override these defaults by calling spdlog::init_thread_pool with custom queue sizes and thread counts before creating loggers. This global pool is shared across all async loggers in the application, amortizing thread creation costs and centralizing message dispatching.

Manual Thread Pool Configuration

Custom pools support explicit sizing and optional lifecycle callbacks. The init_thread_pool overloads accept queue size, thread count, and optional start/stop function objects, allowing integration with existing application thread management strategies.

Message Enqueueing and the Lock-Free Queue

Each async_logger inherits from the base logger class but overrides the sink_it_ method. When a log call occurs, the implementation in include/spdlog/async_logger-inl.h pushes a copy of the log_msg onto the thread pool's lock-free queue via backend_sink_it_.

This design ensures producer threads never block on I/O operations, returning immediately after enqueueing. The lock-free queue implementation minimizes contention between multiple producer threads and the background consumer workers.

Overflow Policies: Block, Overrun Oldest, and Discard New

Defined in include/spdlog/async_logger.h (lines 21-27), the async_overflow_policy enum controls behavior when the queue saturates. The factory functions in include/spdlog/async.h (lines 34-60) bind specific policies to logger creation methods.

Block Policy

block pauses the calling thread using a condition variable until space becomes available in the queue. This is the default for spdlog::create_async (via async_factory), guaranteeing message delivery at the potential cost of application latency if the consumer cannot keep pace with producers.

Overrun Oldest Policy

overrun_oldest drops the oldest queued message to make room for the new entry. This policy is used by spdlog::create_async_nb (via async_factory_nonblock) and suits latency-sensitive applications where recent log data matters more than historical completeness.

Discard New Policy

discard_new silently drops the incoming message if the queue is full, preventing any producer blocking while potentially losing current log data. This is the most aggressive policy for maintaining application throughput at the expense of log completeness.

Worker Thread Processing and Backend Sink

The thread pool implementation in src/async.cpp runs background worker threads that continuously dequeue messages and invoke backend_sink_it_ on the source async_logger. This method bypasses the standard sink_it_ path, forwarding directly to the logger's sinks without the locking contention present in synchronous paths.

Worker threads process messages sequentially per thread, but multiple threads operate concurrently, allowing parallel writes to different sinks or files.

Graceful Shutdown and Memory Safety

The queue stores messages with shared_ptr references to their originating logger, preventing destruction while pending work exists. As noted in include/spdlog/async.h (lines 6-15), this ensures all enqueued messages are processed before the logger is destroyed, preventing log loss during application shutdown.

When the thread pool stops, it drains the queue completely before joining worker threads, guaranteeing that no messages are lost to premature termination.

Complete Implementation Example

The following example demonstrates custom thread pool configuration and different overflow policies:

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

int main() {
    // 1️⃣  Create a custom thread pool: 16k queue, 2 worker threads
    spdlog::init_thread_pool(16 * 1024, 2);

    // 2️⃣  Build an async logger that blocks when the queue is full (default)
    auto logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>(
        "async_file", "logs.txt");

    // 3️⃣  Build a non-blocking logger that discards the oldest entry on overflow
    auto nb_logger = spdlog::create_async_nb<spdlog::sinks::basic_file_sink_mt>(
        "async_nb", "nb_logs.txt");

    // 4️⃣  Log normally – calls return immediately after enqueuing
    for (int i = 0; i < 10000; ++i) {
        logger->info("Message {}", i);          // blocks only if queue is full
        nb_logger->warn("NB message {}", i);    // never blocks, may drop old msgs
    }

    // 5️⃣  Flush and shutdown (optional – destructor handles it)
    logger->flush();
    nb_logger->flush();
}

Key implementation details in this example:

  • init_thread_pool sets a global pool that all subsequent async loggers share
  • create_async uses the block overflow policy (async_overflow_policy::block)
  • create_async_nb uses the overrun_oldest policy (async_overflow_policy::overrun_oldest)

Summary

  • The global thread pool initializes lazily with 8192 slots and 1 thread, configurable via spdlog::init_thread_pool
  • Three overflow policies control backpressure: block (default), overrun_oldest, and discard_new
  • Worker threads consume from a lock-free queue via backend_sink_it_, avoiding the locking overhead of synchronous logging
  • Shared ownership through shared_ptr guarantees message delivery before logger destruction occurs
  • All async loggers share the global thread pool, making thread count and queue size critical tuning parameters for high-throughput applications

Frequently Asked Questions

What is the default queue size in spdlog async logging?

The default queue size is 8192 items, created automatically when the first async logger is instantiated unless overridden by spdlog::init_thread_pool. This size represents a balance between memory consumption and burst absorption capacity.

How do I configure spdlog to drop old messages instead of blocking?

Use spdlog::create_async_nb instead of spdlog::create_async, which applies the overrun_oldest policy. Alternatively, manually construct the logger with async_overflow_policy::overrun_oldest to ensure the oldest queued entry is discarded when the queue reaches capacity.

Can I use multiple threads with spdlog async logging?

Yes, call spdlog::init_thread_pool(queue_size, n_threads) before creating loggers. The thread pool is global and shared across all async loggers, with worker threads processing messages in parallel. Increasing thread count helps when writing to multiple sinks or slow I/O devices.

Does async logging guarantee all messages are written before application exit?

Yes, because each queued message holds a shared_ptr to its logger, destruction is deferred until the thread pool processes all pending entries. This ensures no log loss during graceful shutdown, provided the application allows the thread pool to drain before terminating.

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 →