Understanding Async Logging in spdlog: Thread Pools and Overflow Policies
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). You can override these defaults manually by calling spdlog::init_thread_pool before creating any loggers, as implemented in include/spdlog/async.h lines 75-94.
// 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. 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 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 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 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:
#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_poolestablishes the global pool shared by all async loggers.create_asyncinstantiates loggers with the block overflow policy.create_async_nbinstantiates 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), anddiscard_new(drop newest) based on latency versus durability requirements. - Factory functions:
create_asyncuses blocking behavior;create_async_nbuses non-blocking overrun behavior. - Shutdown safety: Reference counting via
shared_ptrensures loggers persist until the thread pool drains their pending messages. - Configuration: Call
spdlog::init_thread_poolbefore 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, 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, 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.
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 →