# Synchronous vs Asynchronous Logger Performance in spdlog: A Complete Guide

> Discover synchronous vs asynchronous logger performance in spdlog. Understand how background threads and queues impact caller latency for faster logging.

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

---

**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/main/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:

```cpp
// 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:

1. **Level check** — `should_log(lvl)` validates the message passes the configured threshold
2. **Message construction** — A `log_msg` object is populated with timestamp, level, and formatted text
3. **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/main/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:

```cpp
// 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_msg` into 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/main/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/main/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 cost
- **`overrun_oldest`** — Discards oldest queued message; **favors recency over completeness**
- **`discard_new`** — Drops incoming message; **preserves queued work**

Factory functions in [[`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h)](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/async.h#L31-L70) configure these policies:

```cpp
#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

```cpp
#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

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/spdlog/spdlog.h) | None |
| Synchronous, single-thread | `spdlog::basic_logger_st()` | [`spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/spdlog/spdlog.h) | None |
| Asynchronous, blocking | `spdlog::create_async<>()` | [`spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/spdlog/async.h) | Global default |
| Asynchronous, non-blocking | `spdlog::create_async_nb<>()` | [`spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/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.