# spdlog Async vs Synchronous Logging Performance: Complete Technical Comparison

> spdlog async vs synchronous logging: Discover which performs better. Understand how asynchronous logging boosts throughput and lowers latency by offloading I/O to background threads.

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

---

**Asynchronous logging in spdlog delivers significantly higher throughput and lower latency than synchronous logging by offloading I/O operations to a background thread pool via a lock-free queue, whereas synchronous loggers block the calling thread until all sinks complete processing.**

The `gabime/spdlog` library provides two distinct logger architectures designed for different performance requirements. Understanding the trade-offs between **synchronous** (`spdlog::logger`) and **asynchronous** (`spdlog::async_logger`) modes is critical for optimizing application performance, particularly in high-frequency logging scenarios or latency-sensitive systems.

## Core Architectural Differences

The fundamental distinction lies in how each logger handles the path from log call to sink output.

### Synchronous Logger Execution Path

In [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h), the synchronous logger implements a direct execution model. When you invoke a logging method, the `log_it_` function (lines 68-73) immediately iterates through all attached sinks and calls `sink_it_` directly. The calling thread blocks until every sink finishes writing the message to its destination—whether that is a console, file, or network endpoint.

This design ensures deterministic ordering and immediate error feedback but forces the application to pay the full I/O cost synchronously.

### Asynchronous Logger Execution Path

The `spdlog::async_logger` defined in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) inherits from the base logger but overrides the `sink_it_` method. Instead of writing directly, it constructs a `details::async_msg` object and posts it to a thread pool via `post_log`. The calling thread only pays the cost of message formatting and a lock-free enqueue operation before continuing execution.

A dedicated background thread pool, implemented in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h), continuously dequeues messages from an `mpmc_blocking_queue` (multi-producer/multi-consumer queue defined in [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h)) and handles the actual sink I/O operations.

## Performance Characteristics Comparison

### Latency vs Throughput Trade-offs

**Synchronous logging** exhibits high per-call latency because the caller waits for disk or network I/O to complete. Throughput becomes limited by the slowest sink—for example, a spinning disk file sink can bottleneck the entire application.

**Asynchronous logging** minimizes caller latency to just the time required for formatting and queue insertion. Throughput increases significantly because the producer thread is decoupled from I/O operations. Benchmarks typically show several-fold improvements in message rates when using async loggers with sufficient queue depth.

### Memory Overhead and Buffering

Synchronous loggers maintain minimal memory overhead, relying only on internal buffers within individual sinks.

Asynchronous loggers require additional memory for the message queue. By default, the queue allocates space for **8192 items** (`q_max_items`), plus optional back-trace buffers. You configure this via `spdlog::init_thread_pool(queue_size, thread_count)`.

### Overflow Handling Policies

When the async queue fills, `spdlog::async_logger` applies one of three policies defined in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) (lines 21-27):

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

These policies allow you to choose between reliability (block) and bounded latency (discard) based on application requirements.

## Implementation Details in Source Code

### Synchronous Logger Implementation

The core logic resides in [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h). The `log` method constructs a `details::log_msg` object, then calls `log_it_` which directly invokes `sink_it_` for each attached sink. This happens entirely on the calling thread with no intermediate buffering.

### Asynchronous Logger Implementation

[`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) shows that `async_logger` holds a `std::weak_ptr<details::thread_pool>` and an `async_overflow_policy`. The overridden `sink_it_` method moves the message to the thread pool rather than processing it immediately.

The thread pool in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h) manages worker threads that consume `async_msg` objects. These messages wrap `log_msg` instances with additional metadata indicating whether the message is a log entry, flush request, or termination signal.

## Practical Code Examples

### Synchronous Logger (Baseline)

This example creates a standard console logger where every call blocks until output completes:

```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);
    }
}

```

Each `info` call executes `log_it_` and `sink_it_` directly, blocking until the terminal receives the output.

### Asynchronous Logger (High-Throughput)

This configuration uses a background thread pool to decouple logging from the main execution path:

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

int main() {
    // Initialise a thread pool with a queue of 8k 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>{});
    
    for (int i = 0; i < 1'000'000; ++i) {
        async->info("Iteration {}", i);   // Returns almost immediately
    }

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

```

The loop enqueues messages into the lock-free queue; background threads handle the actual I/O, typically yielding several-fold speedup over the synchronous version.

### Configuring Overflow Policy

Control behavior when the queue saturates by specifying an overflow policy:

```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

```

`discard_new` prevents producer stalls when occasional log loss is acceptable.

## When to Use Each Logger Type

**Choose synchronous logging** for simple applications, low-frequency logging, or scenarios requiring strict deterministic ordering where every log must succeed before the operation continues. The `spdlog::logger` class provides immediate error propagation and simpler debugging at the cost of higher latency.

**Choose asynchronous logging** for high-frequency applications such as game engines, high-throughput servers, or real-time systems where the main thread cannot afford to wait for disk I/O. The `spdlog::async_logger` eliminates contention on sinks and prevents logging from stalling critical execution paths.

## Summary

- **Synchronous loggers** block the caller until all sinks complete, offering deterministic execution but limiting throughput to I/O speeds.
- **Asynchronous loggers** use a lock-free queue (`mpmc_blocking_queue`) and background thread pool to decouple formatting from I/O, significantly reducing latency and increasing throughput.
- The default queue size is **8192 items**, configurable via `init_thread_pool()`.
- Overflow policies (`block`, `overrun_oldest`, `discard_new`) in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) let you handle queue saturation based on reliability requirements.
- Key implementation files include [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h) for sync logic and [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h) for async message processing.

## Frequently Asked Questions

### How much faster is spdlog async compared to synchronous logging?

Asynchronous logging can be **several times faster** than synchronous logging in high-throughput scenarios because the calling thread only pays for message formatting and queue insertion, not disk or network I/O. Actual performance gains depend on sink speed, queue size, and thread pool configuration, but the decoupling typically eliminates I/O bottlenecks from the critical path.

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

By default, the async logger uses the **`block`** policy, causing the calling thread to wait until space becomes available. Alternatively, you can configure **`overrun_oldest`** to drop 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) and set during logger construction.

### Is spdlog::logger thread-safe for concurrent writes?

Yes, `spdlog::logger` is thread-safe for concurrent writes, but multiple threads contend on the sink vector mutex. `spdlog::async_logger` eliminates this contention by serializing access through the lock-free `mpmc_blocking_queue`, making it more efficient for multi-threaded applications with high logging frequency.

### How do I configure the thread pool size for async logging?

Call `spdlog::init_thread_pool(queue_size, thread_count)` before creating async loggers, where `queue_size` defaults to 8192 items and `thread_count` specifies background worker threads. This pool is shared across async loggers and manages the `mpmc_blocking_queue` consumption in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h).