# How spdlog Asynchronous Logging Works Internally: Lock-Free Queues and Thread Pools

> Explore spdlog's asynchronous logging internals. Discover how lock-free queues and thread pools speed up logging by decoupling formatting from I/O operations for faster producer threads.

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

---

**spdlog implements asynchronous logging using a lock-free multi-producer multi-consumer (MPMC) queue and a dedicated thread pool that decouples log message formatting from I/O operations, allowing producer threads to return immediately after enqueuing messages.**

The gabime/spdlog library achieves high-throughput logging by moving expensive sink operations to background worker threads. Understanding how spdlog asynchronous logging works internally reveals an architecture built on lock-free data structures, atomic operations, and careful resource lifecycle management. This design ensures that calls to `logger->info()` or `logger->error()` introduce minimal latency to your application's critical path.

## Core Architecture Components

spdlog's asynchronous system consists of three tightly integrated components that work together to buffer and process log messages without blocking producers.

### The Lock-Free MPMC Queue

At the heart of the system lies `spdlog::details::mpmc_blocking_q<log_msg>`, implemented in [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h). This circular buffer uses atomic head and tail indices to enable multiple threads to enqueue `log_msg` objects simultaneously without mutex contention.

The queue operates as a ring buffer where producers push formatted messages via `queue.push_back(msg)`. When the queue reaches capacity, producers block (or drop messages depending on configuration) until worker threads consume entries via `queue.pop_front()`. This lock-free design eliminates lock contention between application threads and logging threads.

### The Thread Pool

The `spdlog::details::thread_pool` class, defined in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h), manages one or more worker threads that continuously monitor the MPMC queue. You initialize this pool via `spdlog::init_thread_pool(queue_size, thread_count)`, which creates workers that run a tight loop waiting on a condition variable.

Each worker repeatedly calls `queue.pop()` to retrieve pending messages, then forwards them to the underlying synchronous logger's `sink_it_()` method. The thread count and queue size are configurable parameters that let you balance memory usage against throughput requirements.

### The Async Logger Implementation

The `spdlog::async_logger` class, declared in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h), inherits from the base `spdlog::logger` but overrides critical virtual methods `sink_it_()` and `flush_()`. Instead of writing directly to sinks, these methods enqueue `log_msg` objects into the shared thread pool's queue.

Each async logger maintains a pointer to a synchronous "worker" logger that performs the actual formatting and sink dispatching once the message reaches a background thread.

## Message Flow Through the Async Pipeline

When your application calls a logging method, the message traverses a specific pipeline designed to minimize blocking:

1. **Formatting**: The user thread constructs a `log_msg` object (defined in [`include/spdlog/details/log_msg.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/log_msg.h)) containing the pre-formatted message, timestamp, logger name, and severity level.

2. **Enqueuing**: The `async_logger::sink_it_()` method pushes this `log_msg` into the MPMC queue using `queue.push_back(msg)`. This operation uses atomic compare-and-swap instructions rather than locks.

3. **Immediate Return**: The producer thread returns control to the application immediately, having performed only memory allocation and atomic operations—no disk I/O or console flushing.

4. **Background Processing**: A worker thread from the pool wakes up (signaled by the queue's condition variable), pops the message via `queue.pop_front()`, and calls `_worker->sink_it_(msg)` on the synchronous logger.

5. **Sink Output**: The synchronous logger performs final formatting and writes to the configured sinks (files, consoles, or custom destinations).

## Configuring and Using Async Loggers

You create asynchronous loggers through the public API in [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h). The following example demonstrates initializing a thread pool and creating a rotating file logger:

```cpp
#include <spdlog/spdlog.h>
#include <spdlog/async.h>

int main()
{
    // Initialize shared thread pool: queue size 8192, 2 worker threads
    spdlog::init_thread_pool(8192, 2);

    // Create async logger with rotating file sink
    auto async_file = spdlog::create_async<spdlog::sinks::rotating_file_sink_mt>(
        "async_file", "logs/app.log", 1048576 * 5, 3);

    // High-speed logging from main thread returns instantly
    for (int i = 0; i < 10000; ++i)
        async_file->info("Message #{} - result {}", i, i * 42);

    // Graceful shutdown flushes remaining messages
    spdlog::shutdown();
}

```

The `create_async` template function constructs an `async_logger` instance that binds to the global thread pool. The [`include/spdlog/details/registry.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/registry.h) header tracks all created loggers to ensure proper cleanup during program termination.

## Graceful Shutdown and Resource Management

The `spdlog::details::registry` singleton maintains ownership of all logger instances, including async loggers and their associated thread pools. When `spdlog::shutdown()` is called—or when the registry destructor runs during program exit—it stops the thread pool's worker loops and ensures all queued messages are flushed to their respective sinks.

This mechanism prevents log truncation during application shutdown. The registry coordinates with `thread_pool` to drain the MPMC queue completely before destroying the async logger objects, guaranteeing that no messages are lost due to premature termination.

## Summary

- **spdlog asynchronous logging** uses a lock-free MPMC queue in [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h) to buffer messages between producer and consumer threads.
- The `thread_pool` class manages configurable background workers that dequeue messages and delegate formatting to synchronous loggers.
- The `async_logger` overrides `sink_it_()` to enqueue messages rather than performing immediate I/O, eliminating blocking on the hot path.
- Resource cleanup is handled automatically by the registry singleton, which ensures all queued messages flush during `spdlog::shutdown()`.

## Frequently Asked Questions

### What happens when the async queue fills up?

By default, producers block on `queue.push_back()` until space becomes available, ensuring no log messages are silently dropped. You can configure alternative policies such as dropping overflow messages by specifying different queue behaviors when initializing the thread pool, though blocking is the recommended default for reliability.

### How many threads should I allocate to the spdlog thread pool?

For most applications, **one or two worker threads** suffice because logging is I/O-bound rather than CPU-bound. Allocating more threads than available CPU cores typically yields diminishing returns and increases context-switching overhead. Monitor your specific workload—if the queue consistently grows despite high thread counts, you may need faster storage rather than more threads.

### Can async loggers share the same thread pool?

Yes. Multiple async loggers created via `spdlog::create_async()` share the global thread pool initialized by `spdlog::init_thread_pool()`. This design conserves system resources while allowing different loggers with varying sink configurations to utilize the same background processing infrastructure.

### Is the MPMC queue truly lock-free?

The `mpmc_blocking_q` implementation uses atomic operations for enqueue and dequeue, making it lock-free for the core data structure operations. However, it uses a condition variable for blocking semantics when the queue is empty or full, which involves kernel-level synchronization. Therefore, the queue is lock-free in the contended case but not wait-free—it may block threads under specific boundary conditions.