Synchronous vs Asynchronous Logger Performance in spdlog: A Complete Guide
Synchronous loggers execute log calls in the caller's thread with full I/O blocking, while asynchronous loggers offload formatting and sink writes to a background thread pool via a lock-free queue, dramatically reducing caller latency at the cost of memory and queue management overhead.
The spdlog library provides two distinct logger implementations that serve different performance requirements. Understanding their architectural differences helps developers choose the right approach for high-throughput applications, latency-sensitive systems, or resource-constrained environments.
Execution Model Differences
Synchronous Logger: Direct Path
The spdlog::logger class, defined in [include/spdlog/logger.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/logger.h), processes every log call directly in the calling thread:
// From logger.h lines 8-13 - simplified call chain
if (should_log(lvl)) {
log_msg msg{...};
sink_it_(msg); // Direct dispatch to each sink
}
Three sequential steps occur on every log call:
- Level check —
should_log(lvl)validates the message passes the configured threshold - Message construction — A
log_msgobject is populated with timestamp, level, and formatted text - Sink iteration — The logger calls each sink's
log()method, executing formatters and I/O operations synchronously
The calling thread blocks until all sinks complete. For file or network sinks, this means full I/O latency exposure to application code.
Asynchronous Logger: Queued Offload
The spdlog::async_logger in [include/spdlog/async_logger.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/async_logger.h) overrides sink_it_() to enqueue messages instead of processing them:
// From async_logger.h lines 10-14 - conceptual override
void sink_it_(const details::log_msg &msg) override {
// Push copy to thread pool queue
thread_pool_->post_log(shared_from_this(), msg, overflow_policy_);
}
The caller thread performs minimal work:
- Copies the
log_msginto the lock-free MPMC queue - Returns immediately (non-blocking unless queue is full)
A dedicated background thread (or thread pool) continuously dequeues messages via process_next_msg_() in [include/spdlog/details/thread_pool.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/details/thread_pool.h) and executes the actual sink writes.
Performance Characteristics Comparison
| Metric | Synchronous Logger | Asynchronous Logger |
|---|---|---|
| Caller latency | Full formatting + I/O time | Queue insertion time only (~100-500ns typical) |
| Peak throughput | Limited by slowest sink | Decoupled from sink speed via buffering |
| Memory footprint | Transient log_msg per call |
Bounded queue + pending messages |
| CPU usage | Caller-bound, predictable | Background thread overhead |
| Burst handling | Head-of-line blocking | Queue absorbs bursts (with policy tradeoffs) |
The queue insertion cost for async loggers typically measures in hundreds of nanoseconds on modern hardware, versus microseconds to milliseconds for disk or network I/O in synchronous mode.
Overflow Policies and Latency Guarantees
The async_overflow_policy enum in [include/spdlog/async_logger.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/async_logger.h#L21-L27) determines behavior when the bounded queue fills:
block— Producer thread waits until space available; guarantees message delivery at latency costoverrun_oldest— Discards oldest queued message; favors recency over completenessdiscard_new— Drops incoming message; preserves queued work
Factory functions in [include/spdlog/async.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/async.h#L31-L70) configure these policies:
#include <spdlog/async.h>
// Blocking (default) - may stall caller
auto logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>(
"async_block", "app.log");
// Non-blocking with overrun_oldest policy
auto nb_logger = spdlog::create_async_nb<spdlog::sinks::stdout_color_sink_mt>(
"async_nb", std::cout);
Ordering and Thread Safety
Synchronous loggers provide strict ordering per sink — messages arrive at each sink in exact emission order because no concurrency exists between log calls and sink writes.
Asynchronous loggers preserve FIFO ordering per logger — the background worker processes each logger's queue sequentially. When multiple async loggers share a thread pool (the default global pool), their relative ordering is non-deterministic based on dequeue scheduling.
Both implementations are thread-safe for concurrent log calls. The synchronous logger uses mutex protection around sink iteration; the async logger uses lock-free queue operations.
Code Examples: Choosing Your Logger
High-Throughput Async Pattern
#include <spdlog/async.h>
#include <spdlog/sinks/daily_file_sink.h>
int main() {
// 4MB queue, 2 background threads
spdlog::init_thread_pool(8192, 2);
auto daily = spdlog::create_async<spdlog::sinks::daily_file_sink_mt>(
"daily_async", "logs/daily.log", 2, 30);
// Thousands of calls/sec, minimal caller impact
for (int i = 0; i < 1000000; ++i) {
daily->info("Event {}", i);
}
}
Latency-Critical Sync Pattern
#include <spdlog/spdlog.h>
#include <spdlog/sinks/null_sink.h>
int main() {
// Null sink for absolute minimal latency benchmarking
auto null_logger = spdlog::create<spdlog::sinks::null_sink_mt>("null");
// ~50-100ns per call, no allocation, no queue
for (int i = 0; i < 1000000; ++i) {
null_logger->trace("Benchmark {}", i);
}
}
Configuration and Factory Methods
| Logger Type | Factory Function | Header | Thread Pool |
|---|---|---|---|
| Synchronous, multi-thread | spdlog::basic_logger_mt() |
spdlog/spdlog.h |
None |
| Synchronous, single-thread | spdlog::basic_logger_st() |
spdlog/spdlog.h |
None |
| Asynchronous, blocking | spdlog::create_async<>() |
spdlog/async.h |
Global default |
| Asynchronous, non-blocking | spdlog::create_async_nb<>() |
spdlog/async.h |
Global default |
The global thread pool initializes lazily on first async logger creation with default parameters (8192 queue slots, 1 thread). Explicit initialization via spdlog::init_thread_pool(queue_size, thread_count) allows customization before logger creation.
Summary
-
Synchronous loggers (
spdlog::logger) execute complete log processing in the caller thread, providing predictable latency and strict ordering with minimal memory overhead — ideal for low-volume logging or latency-sensitive debug builds. -
Asynchronous loggers (
spdlog::async_logger) enqueue messages to a lock-free queue processed by background threads, decoupling application performance from I/O speed — essential for high-throughput production systems. -
Performance tradeoffs center on caller latency versus memory usage, with overflow policies (
block,overrun_oldest,discard_new) offering tunable guarantees under load.
Frequently Asked Questions
How much faster is asynchronous logging in spdlog?
Benchmarks vary by hardware and sink type, but typical async logger queue insertion costs 100-500 nanoseconds versus 1-10 microseconds for file sinks or 100+ microseconds for network sinks synchronously. The caller latency reduction often exceeds 10x for I/O-bound scenarios. Actual throughput gains depend on queue depth configuration and burst patterns.
When should I use synchronous loggers instead of async?
Choose synchronous loggers when: logging volume is low and predictable; you require strict crash durability (no queued messages lost on process termination); debugging with guaranteed immediate output; or operating in memory-constrained embedded environments where queue allocation is undesirable.
Can async loggers lose messages?
Yes, depending on overflow policy. The block policy guarantees delivery but may stall producers. overrun_oldest and discard_new explicitly drop messages to maintain throughput. Additionally, unclean process termination loses any messages still in the queue before background threads flush them.
Do async loggers preserve message ordering across multiple loggers?
No — ordering is per-logger only. When multiple async_logger instances share the global thread pool, their messages interleave non-deterministically based on queue dequeue timing. For global ordering, use a single async logger or implement custom sequencing in your application.
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 →