# Understanding Async Logging in spdlog: Thread Pools and Overflow Policies

> Master async logging in spdlog with thread pools and overflow policies. Discover how lock-free queues optimize log production and sink consumption for efficient backpressure handling.

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

---

**spdlog implements asynchronous logging through a global thread pool that decouples log production from sink consumption, using lock-free queues with configurable overflow policies to handle backpressure.**

Async logging in spdlog allows applications to achieve high-throughput, low-latency logging by offloading disk I/O to background threads. This article examines the internal architecture of the gabime/spdlog repository, detailing how the global thread pool manages message queues and how overflow policies govern behavior when queues reach capacity.

## How Async Logging Works in spdlog

The asynchronous architecture centers on a **global thread pool** that acts as a mediator between log producers (application threads) and consumers (sink operations).

### Global Thread Pool Initialization

When the first async logger is instantiated, `async_factory_impl::create` queries the registry for an existing thread pool via `details::registry::instance().get_tp()`. If none exists, spdlog constructs a default `details::thread_pool` with a **queue size of 8192 items** and **one worker thread** ([`src/async.cpp`](https://github.com/gabime/spdlog/blob/main/src/async.cpp)). You can override these defaults manually by calling `spdlog::init_thread_pool` before creating any loggers, as implemented in [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) lines 75-94.

```cpp
// Configure a custom pool: 16k queue, 2 worker threads
spdlog::init_thread_pool(16 * 1024, 2);

```

Once initialized, all subsequent async loggers share this global pool instance.

### 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 statement executes, the async logger copies the `log_msg` and pushes it onto the thread pool's lock-free queue rather than writing directly to sinks. This operation occurs in `backend_sink_it_` within [`include/spdlog/async_logger-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger-inl.h). The calling thread returns immediately after enqueueing, while worker threads handle the actual disk I/O or network transmission.

## Overflow Policies for Async Loggers

When the lock-free queue reaches capacity, spdlog applies an **overflow policy** defined at logger construction. These policies are enumerated in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) lines 21-27.

### Block Policy

With `async_overflow_policy::block`, the calling thread pauses until space becomes available in the queue. This guarantees every log message is captured but introduces latency during high-volume bursts. The standard `async_factory` uses this policy by default ([`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) lines 34-60).

### Overrun Oldest Policy

The `overrun_oldest` policy discards the oldest message in the queue to make room for the new entry. This prevents application threads from stalling but risks losing historical log data during overflow conditions. Use `create_async_nb` or the `async_factory_nonblock` factory to enable this behavior.

### Discard New Policy

When configured with `discard_new`, the logger drops the incoming message entirely if the queue is full. This minimizes latency impact on the application thread but loses the newest diagnostic information during traffic spikes.

## Worker Thread Processing and Shutdown Behavior

The thread pool spawns one or more background threads that continuously pop messages from the queue and invoke `backend_sink_it_` on the owning `async_logger`. This method bypasses the standard `sink_it_` path, forwarding messages directly to the logger's sinks without contention from producer threads.

### Graceful Shutdown Guarantees

Each queued message maintains a `shared_ptr` to its originating logger, preventing destruction while pending work exists. As noted in the header comments of [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) lines 6-15, this design ensures that `~async_logger` blocks until the thread pool processes all enqueued messages for that logger. Consequently, no log entries are lost during shutdown, provided the process terminates normally.

## Configuring Custom Thread Pools

While spdlog uses a single global thread pool per registry instance, you can customize its parameters via `init_thread_pool` overloads. These allow specification of queue size, thread count, and optional start/stop callbacks. After invocation, all subsequent calls to `create_async` or `create_async_nb` utilize the configured pool rather than the default 8192-slot, single-threaded instance.

## Practical Implementation Example

The following example demonstrates thread pool initialization and the distinction between blocking and non-blocking loggers:

```cpp
#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:

- `init_thread_pool` establishes the global pool shared by all async loggers.
- `create_async` instantiates loggers with the **block** overflow policy.
- `create_async_nb` instantiates loggers with the **overrun_oldest** policy.

## Summary

- **Global thread pool**: spdlog creates a default pool with 8192 queue slots and one worker thread when the first async logger is constructed.
- **Overflow policies**: Choose between `block` (wait for space), `overrun_oldest` (drop oldest), and `discard_new` (drop newest) based on latency versus durability requirements.
- **Factory functions**: `create_async` uses blocking behavior; `create_async_nb` uses non-blocking overrun behavior.
- **Shutdown safety**: Reference counting via `shared_ptr` ensures loggers persist until the thread pool drains their pending messages.
- **Configuration**: Call `spdlog::init_thread_pool` before logger creation to customize queue depth and worker thread count.

## Frequently Asked Questions

### What is the default queue size for spdlog async logging?

The default queue size is **8192 items** with **one worker thread**. These defaults are hardcoded in `async_factory_impl::create` within [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h), though you can override them by calling `spdlog::init_thread_pool` with custom parameters before instantiating any loggers.

### How does spdlog prevent log loss during shutdown?

Each enqueued message holds a `shared_ptr` to its parent logger, preventing destruction while messages remain unprocessed. According to the implementation in [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h), the destructor blocks until the thread pool completes processing all pending entries for that logger, ensuring no log data is lost during normal application termination.

### What is the difference between create_async and create_async_nb?

`spdlog::create_async` uses the `async_factory` which applies the **block** overflow policy, causing producer threads to wait if the queue fills. `spdlog::create_async_nb` uses `async_factory_nonblock` which applies the **overrun_oldest** policy, allowing the application to continue executing at the cost of potentially discarding older log messages.

### Can I use multiple thread pools in the same spdlog application?

No. The spdlog registry maintains a **single global thread pool** instance shared across all async loggers. While you cannot create separate pools for different loggers, you can configure this global pool with custom queue sizes and thread counts via `spdlog::init_thread_pool`, and all subsequent async loggers will utilize that configured instance.