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

> Explore async logging in spdlog. Understand thread pools and overflow policies like block, overrun_oldest, and discard_new for efficient log handling. Learn how spdlog decouples log production from consumption.

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

---

**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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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:

```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 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.