# spdlog vs Synchronous Logging Performance Differences: A Complete Architectural Guide

> Explore spdlog vs synchronous logging performance. Discover how async spdlog decouples formatting from I/O for reduced latency and higher throughput, outperforming synchronous loggers.

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

---

**Asynchronous spdlog loggers using `spdlog::async_logger` decouple message formatting from I/O operations by pushing messages to a lock-free queue processed by a background thread pool, dramatically reducing latency and increasing throughput compared to synchronous `spdlog::logger` instances that block the caller until sinks complete writing.**

The **[gabime/spdlog](https://github.com/gabime/spdlog)** library offers two distinct execution paths for logging operations, each optimized for different performance requirements. Understanding the architectural differences between **synchronous** and **asynchronous** logging modes is essential for maximizing application throughput while managing the trade-offs between latency and resource consumption.

## Execution Path: Blocking vs Non-Blocking

The fundamental distinction between spdlog's synchronous and asynchronous modes lies in how messages flow from the application thread to the final output sink.

**Synchronous execution** (`spdlog::logger`) follows a direct path: the calling thread formats the message, then immediately forwards it to every attached sink. The thread blocks until all sinks complete their write operations. In [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h), the `log_it_` method iterates through the sink vector and invokes `sink_it_` directly on each entry, forcing the caller to pay the full I/O cost for every message.

**Asynchronous execution** (`spdlog::async_logger`) introduces a decoupling layer: the calling thread formats the message and pushes it into a **lock-free queue**, then returns immediately. A dedicated background thread (managed by `spdlog::details::thread_pool`) dequeues the message and handles the actual sink I/O operations. This architecture eliminates blocking I/O from the critical path of the producer thread.

## Performance Characteristics Compared

### Latency and Throughput

**Synchronous loggers** exhibit high latency because every log call incurs the full cost of disk or console I/O. Throughput becomes limited by the slowest sink—for example, writing to a file on a spinning disk creates a hard bottleneck that constrains the entire application.

**Asynchronous loggers** achieve low latency by minimizing the work done in the caller thread to only message formatting and queue insertion. 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) enables multiple producer threads to enqueue messages with minimal contention. Throughput can increase several-fold because the producer thread operates independently of I/O completion.

### Memory Overhead

The synchronous approach maintains minimal memory overhead, using only the internal buffering provided by individual sinks. The asynchronous path requires pre-allocated memory for the lock-free queue, defaulting to **8192 items** (`q_max_items`), plus additional storage for optional back-trace buffers. This trade-off exchanges memory for performance predictability.

## Architectural Implementation Details

### Synchronous Logger Core

The base `spdlog::logger` class defined in [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h) implements the blocking behavior. When an application calls a log method, the implementation constructs a `details::log_msg` object, checks the severity level, and invokes `log_it_`. This method directly calls `sink_it_` for each registered sink (lines 68-73 in the source), forcing the caller to wait until file writes, console output, or network operations complete.

### Asynchronous Logger Wrapper

The `spdlog::async_logger` class 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 critical `sink_it_` method. Instead of writing directly, it captures a `std::weak_ptr<details::thread_pool>` and pushes the message using the thread pool's `post_log` method. The logger also stores an `async_overflow_policy` to determine behavior when the queue reaches capacity.

### Thread Pool and Queue Mechanism

The `spdlog::details::thread_pool` defined in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h) manages a configurable number of worker threads that consume `async_msg` objects from a multi-producer, multi-consumer queue. The queue uses the `mpmc_blocking_queue` implementation, providing lock-free enqueue operations for producer threads while background workers handle the blocking I/O work.

The `details::async_msg` structure (defined within [`thread_pool.h`](https://github.com/gabime/spdlog/blob/main/thread_pool.h), lines 25-68) wraps the original `log_msg` with an `async_msg_type` tag indicating whether the message is a standard log entry, a flush request, or a termination signal. This design allows the thread pool to handle control operations alongside regular logging traffic.

## Overflow Handling Policies

When the asynchronous queue fills, spdlog provides three configurable policies defined in `async_overflow_policy` ([`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) lines 21-27):

- **block** – The caller waits until space becomes available in the queue. This preserves message integrity but potentially stalls the producer.
- **overrun_oldest** – Discards the oldest queued message to make room for the new entry.
- **discard_new** – Drops the incoming message without blocking, useful when occasional log loss is preferable to application stalls.

The default **block** policy ensures no log messages are lost, but latency-sensitive applications may prefer `discard_new` or `overrun_oldest` to maintain throughput under high load.

## Practical Implementation Examples

### Baseline Synchronous Logging

Create a standard console logger where every call blocks until output completes:

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

int main() {
    auto console = spdlog::stdout_color_mt("console");
    
    for (int i = 0; i < 1'000'000; ++i) {
        console->info("Iteration {}", i);  // Blocks until written
    }
}

```

### High-Throughput Asynchronous Setup

Initialize a thread pool and create an async logger to decouple I/O from the producer thread:

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

int main() {
    // Queue size: 8192 items, 2 worker threads
    spdlog::init_thread_pool(8192, 2);
    
    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 immediately after queueing
    }
    
    async->flush();  // Ensure all messages are written before exit
}

```

### Configuring Overflow Behavior

Explicitly set the overflow policy to drop new messages rather than block:

```cpp
auto async = std::make_shared<spdlog::async_logger>(
    "async",
    spdlog::sinks_init_list{std::make_shared<spdlog::sinks::stdout_color_sink_mt>()},
    std::weak_ptr<spdlog::details::thread_pool>{},
    spdlog::async_overflow_policy::discard_new);

```

## Summary

- **Synchronous loggers** (`spdlog::logger`) in [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h) block the caller until all sinks complete I/O, providing deterministic ordering but limiting throughput to the slowest sink's speed.
- **Asynchronous loggers** (`spdlog::async_logger`) in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) use a lock-free queue and background thread pool to eliminate blocking I/O from the producer thread, significantly reducing latency.
- The **thread pool** implementation 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 from the `mpmc_blocking_queue`, enabling high-throughput scenarios.
- **Overflow policies** (`block`, `overrun_oldest`, `discard_new`) control behavior when the default 8192-item queue fills, allowing developers to choose between reliability and performance under load.

## Frequently Asked Questions

### How do I choose between synchronous and asynchronous logging in spdlog?

Choose **synchronous logging** when you require deterministic message ordering, simple debugging, or operate in low-frequency logging scenarios where I/O latency is acceptable. Choose **asynchronous logging** for high-throughput applications, game engines, or latency-sensitive servers where the main thread cannot afford to wait for disk or network I/O operations to complete.

### What is the default queue size for spdlog async loggers?

The default queue size is **8192 items** when initializing the thread pool via `spdlog::init_thread_pool`. You can configure this value based on your application's memory constraints and logging burst patterns, though larger queues consume more memory to store formatted messages awaiting processing.

### Does asynchronous logging in spdlog guarantee that all messages are written before program termination?

No, asynchronous logging requires explicit synchronization. Call `flush()` on the logger or use `spdlog::shutdown()` before exiting to ensure the background thread pool processes all queued messages. Without this step, messages remaining in the `mpmc_blocking_queue` when the application exits will be lost.

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

By default, the logger uses the **block** policy, causing the calling thread to wait until space becomes available. Alternatively, you can configure `async_overflow_policy::overrun_oldest` to discard the oldest message or `async_overflow_policy::discard_new` to drop the incoming message, preventing producer stalls at the cost of potentially losing log data.