# How spdlog's Thread Pool Handles Queue Overflow and Message Persistence

> Learn how spdlog's thread pool manages queue overflow with configurable policies like blocking, discarding, or overwriting. Discover message persistence guarantees for your logs.

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

---

**spdlog's thread pool uses a configurable `async_overflow_policy` to either block callers, discard new messages, or overwrite old ones when its bounded MPMC queue reaches capacity, while guaranteeing that retained messages persist until processed by worker threads.**

The [gabime/spdlog](https://github.com/gabime/spdlog) library routes asynchronous log entries through a lock-free thread pool backed by a fixed-size multi-producer/multi-consumer (MPMC) blocking queue. Understanding how this queue handles saturation and persists messages is essential for building reliable high-throughput logging systems.

## The Asynchronous Logging Architecture

When an `async_logger` receives a log call, it delegates the work to the `thread_pool` via `post_log()` or `post_flush()`. Both methods eventually invoke the private helper `post_async_msg_()` defined in [`include/spdlog/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool-inl.h).

The pool stores messages in `details::mpmc_blocking_queue<async_msg>`, which wraps a `circular_q<T>` buffer sized at construction (`q_max_items`). This circular buffer guarantees FIFO ordering for messages that remain in the queue until worker threads consume them.

## Queue Overflow Policies

spdlog defines three distinct behaviors for handling a full queue through the `async_overflow_policy` enum declared in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h). The default policy is `block`.

### Block Policy (Default)

When configured with `async_overflow_policy::block`, the calling thread waits until the queue has available space. In `post_async_msg_()`, this policy triggers `q_.enqueue()`, which blocks on an internal condition variable until a worker thread frees capacity.

This ensures no message loss but may reduce application throughput during queue saturation.

### Overrun Oldest Policy

Setting `async_overflow_policy::overrun_oldest` causes the newest message to be inserted immediately, evicting the oldest entry in the circular buffer. The implementation calls `q_.enqueue_nowait()`, which pushes without checking capacity, allowing the circular buffer to overwrite the eldest element.

Users can monitor message loss via `thread_pool::overrun_counter()`, which tracks how many times older entries have been overwritten.

### Discard New Policy

With `async_overflow_policy::discard_new`, the thread pool drops incoming messages when the queue is full. The `post_async_msg_()` function invokes `q_.enqueue_if_have_room()`; if the queue lacks space, the internal `discard_counter_` increments and the message is silently rejected.

Retrieve the total discarded count through `thread_pool::discard_counter()`.

## Message Persistence Guarantees

Once a message enters the queue, it persists until a worker thread successfully processes it. Worker threads repeatedly call `process_next_msg_()` in [`include/spdlog/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool-inl.h), which blocks on `q_.dequeue()` until a message becomes available.

During graceful shutdown, the pool posts a `terminate` message for each worker thread using the `block` policy. This ensures all queued messages process before threads join, preventing data loss during application exit.

## Practical Implementation Examples

The following examples demonstrate configuring each overflow policy:

```cpp
// 1. Default block policy - caller waits when queue is full
auto tp = std::make_shared<spdlog::details::thread_pool>(8192, 2);
auto logger = std::make_shared<spdlog::async_logger>(
    "blocking_logger", spdlog::sinks::stdout_sink_mt::instance(),
    tp, spdlog::async_overflow_policy::block);

logger->info("This call blocks if the 8192-item queue is saturated");

// 2. Overrun-oldest policy - new messages overwrite old ones
auto logger_overrun = std::make_shared<spdlog::async_logger>(
    "overrun_logger", spdlog::sinks::stdout_sink_mt::instance(),
    tp, spdlog::async_overflow_policy::overrun_oldest);

for (int i = 0; i < 10000; ++i) {
    logger_overrun->info("msg {}", i);  // Overwrites when full
}
std::cout << "Overruns: " << tp->overrun_counter() << std::endl;

// 3. Discard-new policy - excess messages are dropped
auto logger_discard = std::make_shared<spdlog::async_logger>(
    "discard_logger", spdlog::sinks::stdout_sink_mt::instance(),
    tp, spdlog::async_overflow_policy::discard_new);

for (int i = 0; i < 10000; ++i) {
    logger_discard->info("msg {}", i);  // May be discarded
}
std::cout << "Discarded: " << tp->discard_counter() << std::endl;

```

## Summary

- **spdlog's thread pool** uses a bounded MPMC queue (`mpmc_blocking_queue`) with a configurable overflow policy to handle saturation.
- **Three policies** control behavior: `block` (wait for space), `overrun_oldest` (overwrite oldest), and `discard_new` (drop new).
- **Persistence** is guaranteed for retained messages through blocking dequeue operations in worker threads and graceful shutdown sequences.
- **Diagnostics** are available via `overrun_counter()` and `discard_counter()` to monitor message loss under load.

## Frequently Asked Questions

### What happens to log messages when the spdlog queue is full?

The behavior depends on the `async_overflow_policy` set during logger creation. The `block` policy pauses the caller until space frees up, `overrun_oldest` overwrites the oldest queued message with the new one, and `discard_new` silently drops the incoming message. You can monitor losses using `thread_pool::overrun_counter()` or `discard_counter()`.

### How do I prevent message loss in spdlog async logging?

Use the default `block` overflow policy, which ensures the calling thread waits until the queue has capacity. Additionally, size your thread pool queue appropriately using the `q_max_items` parameter during `thread_pool` construction to match your application's peak logging volume.

### Where does spdlog store async log messages before processing?

Messages are stored in `details::mpmc_blocking_queue<async_msg>`, located in [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h). This lock-free structure wraps a circular buffer (`circular_q`) that holds messages until worker threads dequeue them via `process_next_msg_()`.

### Does spdlog guarantee all messages are flushed on application shutdown?

Yes. During thread pool destruction in [`include/spdlog/details/thread_pool-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool-inl.h), the pool posts a `terminate` message for each worker using the `block` policy. This ensures all preceding messages process before the worker threads join, though messages may still be lost if using `discard_new` or `overrun_oldest` policies during runtime saturation.