How spdlog's Thread Pool Handles Queue Overflow and Message Persistence
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 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.
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. 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, 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:
// 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), anddiscard_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()anddiscard_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. 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, 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →