spdlog vs Synchronous Logging Performance Differences: A Complete Architectural Guide

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

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

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

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

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 →