# spdlog Synchronous vs Asynchronous Logging Performance Comparison: A Complete Guide

> Compare spdlog synchronous vs asynchronous logging performance. Discover how asynchronous logging boosts throughput and reduces latency for high-frequency applications.

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

---

**Synchronous logging blocks the caller until messages are written to sinks, while asynchronous logging decouples formatting from I/O using a lock-free queue, significantly reducing latency and increasing throughput in high-frequency scenarios.**

The *spdlog* library (gabime/spdlog) provides two distinct logging modes that offer different performance trade-offs. Understanding the architectural differences between **synchronous** and **asynchronous logging** is essential for optimizing application latency and throughput. This guide examines the implementation details, performance characteristics, and configuration options based on the actual source code.

## How Synchronous Logging Works in spdlog

The synchronous logger (`spdlog::logger`) is defined in [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h). When you invoke a log method, the calling thread executes the full write path immediately.

According to the source code, the `log` method constructs a `details::log_msg` object and invokes `log_it_`, which iterates through all attached sinks via `sink_it_` ([lines 68-73](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/logger.h#L68-L73)).

This means the **caller blocks until every sink completes its I/O operation**. For file sinks or remote network destinations, this creates significant latency on the critical path.

### Performance Characteristics of Synchronous Logging

Synchronous logging exhibits **high latency** because the application thread pays the full cost of formatting and I/O for every message. Throughput is **limited by the slowest sink** in the chain, making this mode unsuitable for high-frequency logging scenarios. However, memory overhead remains minimal since no additional buffering occurs beyond what individual sinks allocate internally.

## How Asynchronous Logging Works in spdlog

The `spdlog::async_logger` class, defined in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h), inherits from `spdlog::logger` but overrides the `sink_it_` method. Instead of writing directly to sinks, it pushes formatted messages into a **lock-free queue** managed by `spdlog::details::thread_pool` (implemented in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h)).

The thread pool uses a **multi-producer, multi-consumer queue** (`mpmc_blocking_queue` from [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h)) to distribute work across configured worker threads. Background threads continuously dequeue `async_msg` objects and handle the actual sink I/O, allowing the producer thread to return immediately after formatting.

### Queue Management and Overflow Policies

Asynchronous logging requires memory for the message queue. By default, *spdlog* allocates space for **8192 items**, configurable via `init_thread_pool(queue_size, thread_count)`.

When the queue reaches capacity, three overflow policies control behavior (defined in [`async_logger.h`](https://github.com/gabime/spdlog/blob/main/async_logger.h) lines 21-27):

- **block**: The caller waits until space becomes available (default behavior)
- **overrun_oldest**: Discards the oldest queued message to make room for new entries
- **discard_new**: Drops the incoming message if the queue is full

## spdlog Synchronous vs Asynchronous Performance Comparison

### Latency and Throughput Analysis

**Synchronous logging** incurs the full I/O latency on the caller thread, offering minimal memory overhead but limiting throughput to the speed of the slowest sink. **Asynchronous logging** minimizes caller latency to just the cost of formatting and queue insertion, with actual I/O handled by background threads.

In high-throughput scenarios, asynchronous mode can achieve **several times higher throughput** because the producer thread is decoupled from slow I/O operations. Contention moves from the sink vector (synchronous) to the lock-free queue (asynchronous), eliminating blocking on shared resources.

### Memory and Thread Safety Trade-offs

While `spdlog::logger` is thread-safe for concurrent writes, contention occurs on the sink vector. The `spdlog::async_logger` eliminates sink contention entirely; only the lock-free queue is shared between producers and the consumer thread pool.

Asynchronous logging requires additional memory for:

- Pre-allocated queue slots (default 8192 items)
- Optional back-trace buffers
- Dedicated worker threads consuming CPU cycles

## Implementation Examples

### Basic Synchronous Logging

Create a simple console logger where each call blocks until completion:

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

int main() {
    // Create a simple console logger (synchronous)
    auto console = spdlog::stdout_color_mt("console");

    // Tight loop to benchmark
    for (int i = 0; i < 1'000'000; ++i) {
        console->info("Iteration {}", i);
    }
}

```

### High-Throughput Asynchronous Logging

Initialize a thread pool and create an async logger for latency-sensitive applications:

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

int main() {
    // Initialise a thread pool with a queue of 8 k items and 2 worker threads
    spdlog::init_thread_pool(8192, 2);

    // Create an asynchronous logger that uses the pool
    auto async = spdlog::async_logger_mt("async",
                                         spdlog::default_factory::instance(),
                                         std::weak_ptr<spdlog::details::thread_pool>{});
    // Or use the helper:
    // auto async = spdlog::create_async<spdlog::sinks::stdout_color_sink_mt>("async");

    for (int i = 0; i < 1'000'000; ++i) {
        async->info("Iteration {}", i);   // Returns almost immediately
    }

    // Flush remaining messages before exiting
    async->flush();
}

```

### Configuring Overflow Behavior

Control what happens when the async queue fills up:

```cpp
auto async = std::make_shared<spdlog::async_logger>(
    "async",
    spdlog::sinks_init_list{spdlog::sink_ptr{std::make_shared<spdlog::sinks::stdout_color_sink_mt>()}},
    std::weak_ptr<spdlog::details::thread_pool>{},
    spdlog::async_overflow_policy::discard_new); // Drop messages when the queue is full

```

## Summary

- **Synchronous logging** (`spdlog::logger`) blocks the caller during I/O, offering minimal memory overhead but limited throughput and higher latency.
- **Asynchronous logging** (`spdlog::async_logger`) uses a lock-free queue (`mpmc_blocking_queue`) and background thread pool to decouple formatting from I/O, significantly improving throughput.
- Overflow policies (**block**, **overrun_oldest**, **discard_new**) allow customization of behavior when the queue reaches capacity.
- Key source files include [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h) for synchronous logic and [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) with [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h) for the asynchronous implementation.

## Frequently Asked Questions

### When should I use asynchronous logging in spdlog?

Use asynchronous logging when your application requires high-frequency logging or is sensitive to latency, such as in game engines or high-throughput servers. According to the spdlog source code, the async mode prevents the main thread from stalling on slow I/O operations like disk writes or network transmissions.

### How does spdlog's lock-free queue improve performance?

The `mpmc_blocking_queue` implementation in [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h) allows multiple producer threads to enqueue messages without locking, reducing contention compared to synchronous logging where threads compete for the sink vector. This lock-free approach enables the background thread pool to consume messages efficiently.

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

By default, the **block** policy causes the caller to wait until space is available. Alternatively, you can configure **overrun_oldest** to discard the oldest message or **discard_new** to drop the new message. These policies are defined in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) lines 21-27 and set via the `async_overflow_policy` enum.

### Can I mix synchronous and asynchronous loggers in the same application?

Yes, spdlog supports creating both `spdlog::logger` (synchronous) and `spdlog::async_logger` instances simultaneously within the same application. Synchronous loggers write immediately while async loggers use the shared thread pool, allowing you to optimize critical paths with async logging while keeping simpler components synchronous.