How Does Asynchronous Logging in spdlog Work? A Deep Dive into the Thread-Pool Architecture
Asynchronous logging in spdlog decouples log producers from I/O by pushing formatted messages into a lock-free MPMC queue that background worker threads drain to the underlying sinks.
The gabime/spdlog library provides high-performance asynchronous logging through a dedicated thread-pool and lock-free queue. When you call logger->info() on an async logger, the message is enqueued rather than written to disk immediately. This design keeps application threads responsive while preserving the same formatting and sink semantics as synchronous loggers.
Architecture of Asynchronous Logging in spdlog
spdlog implements asynchronous logging by separating the producer (your application thread) from the consumer (the background thread performing I/O). The system is built from four core components that work together to enqueue, route, and flush log messages.
Core Components
spdlog::details::thread_pool– Owns one or more worker threads defined ininclude/spdlog/details/thread_pool.h. Each worker repeatedly callsqueue.pop_front()to retrieve alog_msgand then invokes the target logger’ssink_it_method.spdlog::details::mpmc_blocking_q<log_msg>– A lock-free ring buffer that holds pending log messages. Producers pushlog_msgobjects while consumers pop them without mutex contention.spdlog::async_logger– Inherits fromspdlog::loggerand overridessink_it_andflush_to enqueue messages instead of writing directly. It maintains a pointer to a synchronous logger delegate that handles actual formatting and sink dispatch.spdlog::details::registry– Tracks every created logger globally so that the thread-pool can be shared across multiple async loggers and gracefully shut down on program exit.
Message Flow from Producer to Consumer
The lifetime of a single log call follows a five-step pipeline:
- The user thread formats the message into a temporary
spdlog::details::log_msgobject. - The async logger’s overridden
sink_it_pushes thislog_msginto the MPMC queue viaqueue.push_back(msg). - The producer returns immediately — no disk I/O, no heavy formatting, and no lock contention beyond the lock-free queue.
- A worker thread in the thread-pool pops the message with
queue.pop_front()and forwards it to the synchronous logger via_worker->sink_it_(msg). - The synchronous logger performs final formatting and writes to the configured sinks, such as files or the console.
Queue and Thread-Pool Implementation
The performance of asynchronous logging in spdlog relies on two low-level mechanisms: the lock-free queue and the blocking worker loop.
The Lock-Free MPMC Queue
The queue implementation lives in include/spdlog/details/mpmc_blocking_q.h. It uses a circular buffer with atomic head and tail indices, allowing many producers to enqueue simultaneously without mutexes.
Because the queue is lock-free, multiple application threads can call push_back() in parallel with minimal overhead. If the ring buffer reaches capacity, the producer blocks (or drops the message, depending on the configured queue policy) until a worker thread frees a slot. Under default settings, this guarantees that no log entries are silently lost.
The Worker Thread Loop
The thread-pool is defined in include/spdlog/details/thread_pool.h. You configure it through spdlog::init_thread_pool(queue_size, thread_count), which spins up the requested number of background threads.
Each worker runs a tight loop waiting on a condition variable. When mpmc_blocking_q signals that new items have arrived, a worker wakes, pops a log_msg, and dispatches it. The message structure defined in include/spdlog/details/log_msg.h stores the timestamp, logger name, log level, and pre-formatted payload, so the worker can hand it straight to the underlying sink.
Async Logger API and Usage
spdlog exposes asynchronous logging through the public API in include/spdlog/async.h and the async_logger class declared in include/spdlog/async_logger.h and implemented in include/spdlog/async_logger-inl.h.
Creating an Async Logger
Before creating any async loggers, you must initialize the global thread-pool. After that, spdlog::create_async() builds an async_logger instance that shares the pool.
#include <spdlog/spdlog.h>
#include <spdlog/async.h>
int main()
{
// 1️⃣ Initialise a shared thread-pool (queue size 8192, 2 worker threads)
spdlog::init_thread_pool(8192, 2);
// 2️⃣ Create an asynchronous logger that writes to a rotating file sink
auto async_file = spdlog::create_async<spdlog::sinks::rotating_file_sink_mt>(
"async_file", "logs/app.log", 1048576 * 5, 3);
// 3️⃣ Log from the main thread – this call returns almost instantly
for (int i = 0; i < 10000; ++i)
async_file->info("Message #{} – heavy computation result {}", i, i * 42);
// 4️⃣ Flush and shutdown
spdlog::shutdown();
}
The async_file logger internally constructs a spdlog::async_logger. Every info() call enqueues a lightweight log_msg, and the two worker threads later handle the actual file I/O and rotation.
Graceful Shutdown Behavior
The registry in include/spdlog/details/registry.h maintains ownership of all logger instances. When spdlog::shutdown() is invoked — or when the program exits — the registry stops the thread-pool, wakes any sleeping workers, and ensures all queued messages are flushed to their respective sinks before destruction.
Summary
- Asynchronous logging in spdlog is built on a lock-free
mpmc_blocking_qand a configurablethread_pool. - The
async_loggeroverridessink_it_andflush_to enqueuelog_msgobjects rather than performing I/O on the caller thread. - Worker background threads pop messages from the queue and delegate formatting and sink writes to an internal synchronous logger.
- You initialize the pool once with
spdlog::init_thread_pool()and create loggers withspdlog::create_async(). - Global shutdown is handled automatically by the registry, but explicit
spdlog::shutdown()ensures every queued message is persisted.
Frequently Asked Questions
What happens if the async queue fills up?
If the MPMC queue reaches its capacity, the producer thread blocks on push_back() until a worker thread frees a slot. Depending on the queue policy, spdlog can also drop the message, but under default settings it guarantees that no log entries are silently lost.
How many worker threads should I configure?
The optimal thread count depends on your sink throughput and core count. A single worker thread is often sufficient for disk-bound logging, while multiple workers in spdlog::init_thread_pool() help when multiple sinks or high-concurrency workloads are involved. The pool is shared across all async loggers, so one global pool usually suffices.
Is formatting performed before or after enqueueing?
The application thread performs lightweight message capture into a log_msg structure, but heavy formatting is deferred until the worker thread processes the message. This is why the producer can return quickly while the background thread handles the full string formatting and sink dispatch.
How do I flush an asynchronous logger?
The async_logger overrides flush_() to enqueue a flush command that the thread pool later executes. You can also call spdlog::shutdown() to stop the pool and guarantee that all pending messages and flush operations complete before program termination.
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 →