# How spdlog Asynchronous Logging Works: Internal Architecture and Overflow Policies Explained

> Explore spdlog's asynchronous logging. Understand its internal architecture, lock-free queue, and overflow policies block and overrun_oldest for efficient log offloading.

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

---

**spdlog uses a global thread‑pool with a lock‑free multi‑producer‑multi‑consumer queue to offload log writes from the calling thread, offering two overflow policies—`block` (default) and `overrun_oldest`—that determine behavior when the queue saturates.**

The spdlog library implements asynchronous logging through a carefully designed **two‑stage pipeline**: log messages are enqueued by producer threads and later drained by dedicated worker threads. This architecture minimizes latency in hot paths while providing configurable backpressure handling through its `async_overflow_policy` mechanism.

## Core Components of spdlog's Async Subsystem

### The Global Thread Pool

In [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h), the `thread_pool` class manages one or more worker threads and a bounded `mpmc_blocking_q` queue. The pool is instantiated lazily via `spdlog::init_thread_pool()` or automatically when the first `async_logger` is created.

The queue stores `async_msg` objects (defined in [`thread_pool.h`](https://github.com/gabime/spdlog/blob/main/thread_pool.h)) containing:
- A formatted `log_msg` structure
- A `std::shared_ptr<logger>` reference to prevent premature logger destruction

Worker threads continuously call `pop()` and invoke the underlying sink(s) synchronously—sinks themselves remain unaware of the async machinery.

### The Async Logger Wrapper

[`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) defines `async_logger`, a thin subclass of `logger` that overrides `sink_it_()`. Instead of writing directly to sinks, it calls `thread_pool::push()`, forwarding the message to the shared queue.

The `async_overflow_policy` enum is declared here:

```cpp
enum class async_overflow_policy {
    block,          // Default: wait until queue space available
    overrun_oldest  // Non-blocking: discard oldest message
};

```

### Factory and Policy Injection

[`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) provides `async_factory_impl<OverflowPolicy>`, a template factory that bakes the overflow policy into the logger type at compile time:

```cpp
using async_factory = async_factory_impl<async_overflow_policy::block>;
using async_factory_nonblock = async_factory_impl<async_overflow_policy::overrun_oldest>;

```

The `create_async()` and `create_async_nb()` convenience functions use these aliases.

## How Messages Flow Through the System

1. **Producer path**: `logger->info(...)` → `async_logger::sink_it_()` → `thread_pool::push(async_msg, policy)`
2. **Queue storage**: `mpmc_blocking_q` (in [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h)) handles lock‑free enqueue/dequeue
3. **Consumer path**: Worker thread → `thread_pool::pop()` → `async_logger::backend_sink_it_()` → sink output

The [`include/spdlog/details/registry.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/registry.h) singleton holds the `thread_pool` instance under `tp_mutex`, ensuring thread‑safe initialization and shared access across all `async_logger` instances.

## Overflow Policy Options Explained

When the bounded queue reaches capacity, spdlog's behavior is determined by the template‑selected policy:

### `block` (Default Policy)

The calling thread waits on a condition variable until a consumer frees a slot. Implemented in [`src/async.cpp`](https://github.com/gabime/spdlog/blob/main/src/async.cpp) within the `push()` method.

**Characteristics**:
- Guarantees zero message loss
- Introduces unbounded latency under heavy load
- Safe for critical audit trails

```cpp
// Blocking async logger (default behavior)
auto logger = spdlog::create_async<spdlog::sinks::stdout_color_sink_mt>(
    "blocking_logger");

logger->warn("This may pause if the queue is full");

```

### `overrun_oldest` (Non‑Blocking Policy)

The queue discards its oldest entry via `pop_front()` before enqueueing the new message. The producer thread never stalls.

**Characteristics**:
- Bounded latency regardless of consumer performance
- Oldest messages sacrificed during bursts
- Ideal for telemetry where fresh data matters more than completeness

```cpp
// Non-blocking async logger (drops oldest on overflow)
auto nb_logger = spdlog::create_async_nb<spdlog::sinks::stdout_color_sink_mt>(
    "nonblock_logger");

nb_logger->info("Oldest entries are discarded if queue saturates");

```

## Customizing Thread Pool Configuration

By default, the lazy‑initialized pool uses **8192 queue slots** and **1 worker thread**. Override this with explicit initialization:

```cpp
// Configure before creating any async loggers
spdlog::init_thread_pool(16384, 4);  // 16K slots, 4 threads

auto custom = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>(
    "custom_logger", "app.log");

// All subsequent async loggers share this pool

```

The `init_thread_pool()` function is defined in [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) and locks `registry::tp_mutex_` during setup.

## Key Implementation Files

| File | Responsibility |
|------|---------------|
| [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) | Factory templates, `init_thread_pool()`, policy aliases |
| [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) | `async_logger` class, `async_overflow_policy` enum |
| [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h) | Worker thread management, `async_msg` structure |
| [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h) | Lock‑free bounded queue implementation |
| [`include/spdlog/details/registry.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/registry.h) | Singleton thread‑pool storage, `tp_mutex` |
| [`src/async.cpp`](https://github.com/gabime/spdlog/blob/main/src/async.cpp) | Concrete `push()`/`pop()` with policy‑dependent logic |

## Summary

- spdlog's **asynchronous logging** decouples formatting from I/O via a shared `thread_pool` and lock‑free `mpmc_blocking_q`
- Two compile‑time **overflow policies** control saturation behavior: `block` (wait for space) or `overrun_oldest` (discard oldest)
- The `async_factory_impl<>` template injects policy into logger types; use `create_async()` or `create_async_nb()` for convenience
- Global pool defaults (8192 slots, 1 thread) can be overridden via `spdlog::init_thread_pool()`
- All async loggers share the same registry‑managed pool, minimizing resource overhead

## Frequently Asked Questions

### What happens if I don't call `init_thread_pool()` before creating an async logger?

The first `create_async()` call automatically constructs a default pool with 8192 queue slots and 1 worker thread. This is thread‑safe due to `registry::tp_mutex_` but offers no customization. Explicit initialization is recommended for production deployments.

### Can I mix blocking and non‑blocking async loggers in the same program?

No—all `async_logger` instances share the global `thread_pool`, which is created with a single policy. The `async_overflow_policy` is a template parameter of `async_factory_impl`, so the first logger created effectively locks the policy for the process. Choose based on your most stringent requirement.

### How does `overrun_oldest` affect performance compared to `block`?

`overrun_oldest` eliminates producer‑side contention and latency spikes at the cost of message durability. In [`src/async.cpp`](https://github.com/gabime/spdlog/blob/main/src/async.cpp), the policy check occurs before the condition variable wait—avoiding the `std::unique_lock` and `notify` overhead entirely when the queue is full. Benchmarks typically show 10–100× lower tail latency under saturation.

### Is the queue truly lock‑free?

The `mpmc_blocking_q` in [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h) uses atomic operations for enqueue/dequeue, but **blocking policies** introduce condition variables for backpressure. Thus: data structure operations are lock‑free, but synchronization primitives are used for flow control when `block` is selected.