# How Does Asynchronous Logging in spdlog Work? A Deep Dive into the Thread-Pool Architecture

> Discover how asynchronous logging in spdlog leverages a thread-pool and lock-free queue to decouple log producers from I/O, boosting application performance.

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

---

**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 in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h). Each worker repeatedly calls `queue.pop_front()` to retrieve a `log_msg` and then invokes the target logger’s `sink_it_` method.
- **`spdlog::details::mpmc_blocking_q<log_msg>`** – A lock-free ring buffer that holds pending log messages. Producers push `log_msg` objects while consumers pop them without mutex contention.
- **`spdlog::async_logger`** – Inherits from `spdlog::logger` and overrides `sink_it_` and `flush_` 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:

1. The **user thread** formats the message into a temporary `spdlog::details::log_msg` object.
2. The async logger’s overridden `sink_it_` pushes this `log_msg` into the MPMC queue via `queue.push_back(msg)`.
3. The **producer returns immediately** — no disk I/O, no heavy formatting, and no lock contention beyond the lock-free queue.
4. 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)`.
5. 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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) and the `async_logger` class declared in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) and implemented in [`include/spdlog/async_logger-inl.h`](https://github.com/gabime/spdlog/blob/main/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.

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/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_q` and a configurable `thread_pool`.
- The `async_logger` overrides `sink_it_` and `flush_` to enqueue `log_msg` objects 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 with `spdlog::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.