spdlog Async vs Synchronous Logging Performance: Complete Technical Comparison
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, 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 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, continuously dequeues messages from an mpmc_blocking_queue (multi-producer/multi-consumer queue defined in 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 (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. 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 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 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:
#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:
#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:
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) ininclude/spdlog/async_logger.hlet you handle queue saturation based on reliability requirements. - Key implementation files include
include/spdlog/logger.hfor sync logic andinclude/spdlog/details/thread_pool.hfor 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →