spdlog Synchronous vs Asynchronous Logging Performance Comparison: A Complete Guide

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. 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).

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, 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).

The thread pool uses a multi-producer, multi-consumer queue (mpmc_blocking_queue from 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 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:

#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:

#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:

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 for synchronous logic and include/spdlog/async_logger.h with 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →