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

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

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

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

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

#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, providing predictable latency but potentially blocking application logic.
  • Asynchronous loggers utilize async_logger.h and 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.

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 →