How spdlog's Thread Pool Enables Asynchronous Logging: Architecture Deep Dive
spdlog implements asynchronous logging by off-loading sink I/O operations to a dedicated thread pool, allowing application threads to queue log messages and return immediately without blocking on disk or network writes.
The gabime/spdlog library is a fast C++ logging library whose async logging support is built on a custom thread_pool class. This architecture separates the latency-critical logging API from the potentially slow sink operations, achieving high throughput through lock-free queueing and configurable worker threads.
Core Components of the Async Logging Pipeline
The spdlog thread pool implementation consists of four tightly integrated components:
| Component | Role | Key Source File |
|---|---|---|
async_logger |
Public API that queues log messages and flush requests | include/spdlog/async_logger.h |
thread_pool |
Manages worker threads and the bounded queue | include/spdlog/details/thread_pool.h |
mpmc_blocking_queue |
Lock-free ring buffer for async_msg objects |
include/spdlog/details/mpmc_blocking_q.h |
async_msg |
Wrapper for log messages, flush commands, or termination signals | include/spdlog/details/thread_pool.h |
How Messages Flow Through the System
When your application calls logger->info(), the data flows through three distinct stages:
- Capture —
async_loggerpackages the message without I/O - Queue —
thread_poolinserts the message into thempmc_blocking_queue - Process — Worker threads dequeue and execute sink writes
Queueing Log Calls: The Non-Blocking Frontend
The async_logger class overrides sink_it_() to intercept all log calls. Instead of writing directly to sinks, it delegates to the thread pool:
pool_ptr->post_log(shared_from_this(), msg, overflow_policy_);
In include/spdlog/async_logger-inl.h, this path creates an async_msg of type log containing:
- A
std::shared_ptrto the originating logger - The complete
log_msgpayload (timestamp, level, message, source location) - The message type discriminator
The thread_pool::post_async_msg_() method then enqueues this message according to the overflow policy (block, overrun_oldest, or discard_new). The application thread returns immediately—no disk flush, no mutex contention on sink output, no network latency.
This implementation appears in include/spdlog/details/thread_pool-inl.h at lines 57–63.
Worker Thread Architecture
The thread_pool constructor spawns N worker threads (default 1) that execute a continuous processing loop:
while (process_next_msg_()) { /* loop */ }
Each thread in include/spdlog/details/thread_pool-inl.h (lines 91–115) performs:
- Dequeue — Block on
mpmc_blocking_queueuntil a message arrives - Dispatch — Switch on
async_msg::msg_type:log→worker_ptr->backend_sink_it_(msg)flush→worker_ptr->backend_flush_()terminate→ Returnfalseto exit loop
The backend_sink_it_() function iterates over all attached sinks, checks their individual log levels, and forwards formatted messages. This is the only code path where actual I/O occurs—file writes, console output, or network operations all happen inside these worker threads.
Backend Execution and Sink Interaction
The backend functions defined in include/spdlog/async_logger-inl.h (lines 62–80) transform the queued async_msg back into sink operations:
// Conceptual flow inside backend_sink_it_
for (auto& sink : sinks_) {
if (sink->should_log(msg.level)) {
sink->log(msg); // Actual I/O happens here
}
}
This design achieves thread safety without per-log mutexes: the MPMC queue handles synchronization, and individual sinks operate exclusively within a single worker thread context. Sinks that require internal locking (like stdout_color_sink_mt) remain correct, while lock-free sinks achieve maximum performance.
Graceful Shutdown Mechanics
When the thread_pool destructor runs, it guarantees no log messages are lost:
// From thread_pool-inl.h lines 43-53
for (size_t i = 0; i < threads_.size(); ++i) {
post_async_msg_(async_msg{async_msg_type::terminate},
async_overflow_policy::block);
}
for (auto& t : threads_) {
t.join();
}
Each worker receives a terminate message, processes any remaining queued messages, then exits cleanly. The join() ensures the destructor blocks until all pending I/O completes.
Configurable Overflow Policies
The async_overflow_policy enum controls queue-full behavior:
| Policy | Behavior | Use Case |
|---|---|---|
block |
Caller blocks until queue space available | Reliability-critical applications |
overrun_oldest |
Replace oldest message with newest | Latency-sensitive, can tolerate drops |
discard_new |
Silently drop newest message | High-throughput, loss-tolerant logging |
Policy enforcement occurs in thread_pool::post_async_msg_() at lines 81–88 of include/spdlog/details/thread_pool-inl.h, using the mpmc_blocking_queue methods enqueue(), enqueue_nowait(), and enqueue_if_have_room().
Complete Working Example
#include <spdlog/spdlog.h>
#include <spdlog/async_logger.h>
#include <spdlog/details/thread_pool.h>
#include <spdlog/sinks/basic_file_sink.h>
int main() {
// Create thread pool: 8192-item queue, 2 worker threads
auto tp = std::make_shared<spdlog::details::thread_pool>(
8192, // queue_max_items
2 // worker_threads
);
// Build file sink
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
"async_log.txt"
);
// Create async logger with block-on-full policy
auto logger = std::make_shared<spdlog::async_logger>(
"file_async",
file_sink,
tp,
spdlog::async_overflow_policy::block
);
// Log from main thread — returns immediately, I/O happens in workers
logger->info("Processing request id={}", 42);
logger->warn("High latency detected: {}ms", 150);
// Async flush — posts flush command to queue
logger->flush();
// Graceful shutdown: destroy pool, wait for workers
tp.reset(); // or let it go out of scope
return 0;
}
Key Source Files Reference
| File | Purpose |
|---|---|
include/spdlog/details/thread_pool.h |
thread_pool class declaration, async_msg struct, queue types |
include/spdlog/details/thread_pool-inl.h |
Worker thread creation, message posting, dispatch loop, shutdown |
include/spdlog/async_logger.h |
Public async_logger API and overflow policy definitions |
include/spdlog/async_logger-inl.h |
Backend sink iteration and flush implementation |
include/spdlog/details/mpmc_blocking_q.h |
Lock-free bounded MPMC queue implementation |
Summary
- spdlog's thread pool decouples logging API calls from sink I/O through a bounded lock-free queue
async_loggerpackages messages without blocking;thread_poolworkers execute all sink operations- Worker threads consume
async_msgobjects and invokebackend_sink_it_()orbackend_flush_()exclusively - Overflow policies (
block,overrun_oldest,discard_new) let applications trade reliability for latency - Graceful shutdown guarantees all queued messages are processed before destruction via terminate messages and
join()
Frequently Asked Questions
How does spdlog prevent log message loss during high load?
spdlog's thread pool uses a bounded queue with configurable overflow policy. The default block policy causes the calling thread to wait until queue space is available, ensuring no messages are dropped. For loss-tolerant scenarios, overrun_oldest or discard_new policies apply backpressure by removing messages rather than blocking producers.
What is the optimal number of worker threads for spdlog's async logging?
For single-file sinks, one worker thread is optimal—multiple threads would contend for the same file lock. For multiple distinct sinks or network sinks, match thread count to the number of independent I/O channels. The constructor default of 1 thread suits most workloads; profile your specific sink mix to determine if additional threads improve throughput without increasing contention.
Can I use the same thread pool for multiple async loggers?
Yes. A single thread_pool instance can service multiple async_logger objects, which is the recommended pattern to control total thread count. The pool's MPMC queue handles concurrent enqueues from any number of loggers, and worker threads dispatch to the appropriate logger's backend via the shared_ptr stored in each async_msg.
How does spdlog's async performance compare to synchronous logging?
For fast sinks (memory buffers, null_sink), sync logging avoids queue overhead and is faster. For slow sinks (rotating files, network endpoints, remote syslog), async logging prevents application threads from stalling on I/O latency. The lock-free MPMC queue minimizes enqueue overhead to approximately 50-100 nanoseconds on modern hardware, making async preferable when sink latency exceeds 1 microsecond.
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 →