# Synchronous vs Asynchronous Logging in spdlog: What Is the Difference?

> Understand synchronous vs asynchronous logging in spdlog. Learn how spdlog handles log messages in the caller's thread or a background pool to optimize performance.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: deep-dive
- Published: 2026-07-22

---

**Synchronous loggers process messages in the caller's thread immediately, while asynchronous loggers enqueue messages to a lock-free queue processed by a background thread pool, trading latency for throughput.**

The spdlog library (gabime/spdlog) provides two distinct architectures for handling log output that differ fundamentally in threading behavior and performance characteristics. Understanding the difference between synchronous and asynchronous logging in spdlog is essential for optimizing application throughput and responsiveness. Each approach handles message dispatch, resource allocation, and ordering guarantees differently according to the implementation in the source headers.

## How Synchronous Logging Works

In the synchronous model, every log call executes entirely within the caller's thread context. When you invoke a logging method, the library immediately checks the log level, constructs the message, and dispatches it to the configured sinks.

### Core Implementation in logger.h

The synchronous implementation resides in [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h). When a log function is called, the logger performs three sequential operations:

1. **Level filtering** – The `should_log(lvl)` method validates whether the message meets the current threshold.
2. **Message dispatch** – The `sink_it_(msg)` method forwards the fully-formed `log_msg` to each registered sink.
3. **Formatting** – Each sink applies its own formatter before writing to the destination.

Because all processing occurs in the calling thread, the application blocks until the I/O operation completes. This provides strict ordering guarantees but can become a bottleneck under high load.

## How Asynchronous Logging Works

Asynchronous loggers decouple the log call from the I/O operation by introducing an intermediary queue. Instead of writing directly to sinks, the logger enqueues a copy of the message and returns control to the caller immediately.

### The Lock-Free Queue Architecture

The async implementation in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h) inherits from the base logger but overrides the `sink_it_` method to push messages into a **lock-free MPMC queue** via `thread_pool::post_log`. A dedicated background thread pool, defined in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h), continuously dequeues messages through `process_next_msg_()` and executes the actual sink writes.

This architecture requires a `thread_pool` object and a bounded queue (default size 8192 with one worker thread), trading memory overhead for significantly reduced caller-side latency.

### Overflow Handling Policies

When the queue reaches capacity, spdlog applies one of three behaviors defined by the `async_overflow_policy` enum in [`async_logger.h`](https://github.com/gabime/spdlog/blob/main/async_logger.h):

- **block** – The producer thread waits until space becomes available.
- **overrun_oldest** – The oldest queued message is discarded to accommodate the new entry.
- **discard_new** – The incoming message is dropped if the queue is full.

## Performance and Architectural Comparison

| Aspect | Synchronous Logger | Asynchronous Logger |
|--------|-------------------|---------------------|
| **Execution Thread** | Caller thread blocks until I/O completes | Caller thread enqueues and returns immediately |
| **Throughput Impact** | High latency on log calls; scales with I/O speed | Minimal caller latency; scales with CPU and queue depth |
| **Resource Usage** | No additional threads or queues | Requires thread pool and bounded queue memory |
| **Ordering** | Strict global ordering | FIFO per logger; shared pools may interleave messages |
| **Thread Safety** | Thread-safe but blocking | Thread-safe and non-blocking (unless policy is `block`) |

## Implementation Examples

### Creating a Synchronous Logger

Use the standard factory functions to create a logger that processes messages immediately:

```cpp
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>

int main() {
    auto logger = spdlog::basic_logger_mt("sync_file", "logs/sync.log");
    logger->info("Processed in caller thread");
    logger->flush();  // Explicit flush blocks until written
}

```

### Creating an Asynchronous Logger

Include the async header and use `create_async` to leverage the global thread pool:

```cpp
#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>

int main() {
    auto async_logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>(
        "async_file", "logs/async.log");
    
    async_logger->info("Enqueued and returns immediately");
    async_logger->flush();  // Sends flush request to background thread
}

```

### Non-Blocking Configuration with Overflow Handling

For high-throughput scenarios where message loss is acceptable, use `create_async_nb` to select the `overrun_oldest` policy:

```cpp
#include <spdlog/async.h>
#include <spdlog/sinks/stdout_color_sinks.h>

int main() {
    auto nb_logger = spdlog::create_async_nb<spdlog::sinks::stdout_color_sink_mt>(
        "async_nb", std::cout);
    
    for (int i = 0; i < 100000; ++i)
        nb_logger->info("Message {}", i);  // May drop oldest if queue fills
}

```

## Summary

- **Synchronous loggers** execute all formatting and I/O in the calling thread via [`logger.h`](https://github.com/gabime/spdlog/blob/main/logger.h), providing predictable latency but potentially blocking application logic.
- **Asynchronous loggers** utilize [`async_logger.h`](https://github.com/gabime/spdlog/blob/main/async_logger.h) and [`thread_pool.h`](https://github.com/gabime/spdlog/blob/main/thread_pool.h) to offload work to a background thread pool through a lock-free queue.
- The choice between `create_async` (blocking overflow) and `create_async_nb` (discard oldest) determines behavior under backpressure.
- Async mode requires additional memory for the queue and thread pool but eliminates I/O bottlenecks from the critical path.

## Frequently Asked Questions

### Is spdlog thread-safe for synchronous loggers?

Yes. The `spdlog::logger` class is thread-safe for concurrent log calls, meaning multiple threads can invoke logging methods simultaneously. However, each call blocks until the sink completes the write operation, so heavy concurrent logging may serialize threads at the I/O layer.

### What happens when the async queue is full?

Behavior depends on the configured `async_overflow_policy`. With the default `block` policy, the calling thread pauses until queue space frees up. Using `create_async_nb` selects `overrun_oldest`, which discards the oldest message to make room, while `discard_new` simply drops the incoming message.

### Can I use both sync and async loggers in the same application?

Yes. You can instantiate synchronous loggers via `spdlog::basic_logger_mt` and asynchronous loggers via `spdlog::create_async` within the same process. They operate independently; async loggers share a global thread pool initialized lazily on first use, while sync loggers require no additional infrastructure.

### Which logging mode should I choose for high-throughput applications?

For high-throughput scenarios where latency matters more than immediate durability, asynchronous logging with `create_async` is recommended. It offloads formatting and I/O to background threads, preventing the caller from blocking on disk or network writes. If message loss is unacceptable, use the `block` overflow policy with a sufficiently large queue size.