Synchronous vs Asynchronous Loggers in spdlog: Key Differences Explained

Synchronous loggers process log messages directly in the caller thread while asynchronous loggers enqueue messages to a lock-free queue processed by a background thread pool, trading immediate consistency for higher throughput.

The spdlog library provides two distinct logger implementations that differ fundamentally in how they handle message dispatch and I/O operations. Understanding the difference between synchronous and asynchronous loggers in spdlog is essential for optimizing application performance and ensuring reliable logging under varying load conditions. This article examines the architectural distinctions, performance characteristics, and configuration options based on the source code in the gabime/spdlog repository.

Core Architectural Differences

The primary distinction lies in which thread executes the formatting and sink write operations.

Synchronous Logger Implementation

The synchronous logger is defined in [include/spdlog/logger.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/logger.h). When a log call occurs, the implementation follows a direct execution path:

  1. Level check – should_log(lvl) validates the message priority against the logger's configured level
  2. Sink dispatch – sink_it_(msg) forwards the fully-formed log_msg directly to each registered sink
  3. Formatter invocation – Each sink applies its own formatter and performs I/O immediately

Because all processing happens in the caller thread, the method blocks until the underlying write operation completes. This design ensures immediate durability but couples logging latency directly to I/O performance.

Asynchronous Logger Implementation

The asynchronous logger, defined in [include/spdlog/async_logger.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/async_logger.h), inherits from spdlog::logger but overrides the sink_it_ method. Instead of direct dispatch, it performs these steps:

  1. Message queuing – Creates a copy of the log_msg and posts it to the thread pool via thread_pool::post_log
  2. Background processing – Workers in [include/spdlog/details/thread_pool.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/details/thread_pool.h) continuously call process_next_msg_() to dequeue messages
  3. Deferred sink writes – The background thread handles formatting and I/O operations

This architecture decouples the application thread from disk or network latency, but introduces queue management complexity.

Performance and Threading Characteristics

When comparing synchronous and asynchronous loggers in spdlog, consider these operational differences:

  • Execution path – Synchronous loggers execute entirely in the caller thread; asynchronous loggers pay only the cost of memory allocation and queue insertion before returning control
  • Throughput – High-frequency logging scenarios benefit from async loggers because the caller thread never waits for disk flushes or network transmissions
  • Ordering guarantees – Synchronous loggers write messages in exact chronological order. Asynchronous loggers preserve FIFO ordering per-logger, but relative ordering between multiple async loggers sharing a thread pool depends on queue scheduling
  • Resource overhead – Synchronous mode requires no additional threads or queues. Asynchronous mode requires a thread_pool object with a bounded lock-free MPMC queue and associated memory for message storage

Overflow Handling Policies

Asynchronous loggers must handle queue saturation. The async_overflow_policy enum in [include/spdlog/async_logger.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/async_logger.h) defines three strategies:

  • block – The producer thread waits until space becomes available in the queue
  • overrun_oldest – Discards the oldest queued message to make room for the new entry
  • discard_new – Drops the incoming message if the queue is full

The factory functions in [include/spdlog/async.h](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/async.h) configure these policies during logger creation.

Practical Code Examples

Creating a Synchronous Logger

Use the standard factory functions for synchronous operation. The caller blocks on each log call until the sink completes the write.

#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->debug("Debug data written immediately");
    logger->flush();  // Explicit flush blocks until disk commit
}

Creating an Asynchronous Logger (Blocking)

Use spdlog::create_async to initialize a logger with the global thread pool. By default, this uses the block overflow policy.

#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 for background processing");
    async_logger->debug("Returns immediately to caller");
    async_logger->flush();  // Requests flush from background thread
}

The create_async function lazy-initializes a global thread pool (default: 8192 queue slots, 1 thread) and returns a std::shared_ptr<async_logger>.

Non-Blocking Asynchronous Logger

Use spdlog::create_async_nb to select the overrun_oldest policy, ensuring the caller never blocks even under heavy load.

#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("High volume message {}", i);  // May drop oldest if queue full
    }
}

Summary

  • Synchronous loggers execute sink_it_ directly in the caller thread, providing immediate durability but blocking on I/O
  • Asynchronous loggers enqueue messages via thread_pool::post_log and process them in background threads, minimizing caller latency
  • The async_overflow_policy controls behavior when the lock-free queue saturates: blocking, discarding oldest, or discarding new
  • Factory functions in async.h manage the global thread pool lifecycle and overflow configuration
  • Choose synchronous mode for simplicity and strict ordering guarantees; choose asynchronous mode for high-throughput scenarios where caller latency must remain predictable

Frequently Asked Questions

When should I use asynchronous loggers in spdlog?

Use asynchronous loggers when your application requires high-throughput logging or when log sinks involve high-latency operations like network writes or slow disk I/O. The background thread pool absorbs these delays, preventing the main application thread from stalling. However, if you require immediate confirmation that a message has been persisted to disk, synchronous logging provides stronger durability guarantees.

Does spdlog guarantee message ordering with async loggers?

Yes, ordering is preserved per-logger on a FIFO basis. The lock-free MPMC queue in thread_pool.h ensures that messages posted by a specific async_logger instance are processed in the exact order they were enqueued. However, if multiple async loggers share the same thread pool, their relative output ordering depends on the scheduling of the queue consumers and cannot be guaranteed across different logger instances.

What happens when the async logger queue is full?

The behavior depends on the configured async_overflow_policy. With the default block policy, the calling thread waits until space becomes available. With overrun_oldest, the oldest message in the queue is removed to accommodate the new one. With discard_new, the new message is dropped silently. You can select the policy using spdlog::create_async or spdlog::create_async_nb as implemented in include/spdlog/async.h.

Can I mix synchronous and asynchronous loggers in the same application?

Yes. spdlog allows simultaneous use of both logger types. Synchronous loggers instantiated via spdlog::basic_logger_mt operate independently, while asynchronous loggers created through spdlog::create_async share the global thread pool managed by the async factory. This flexibility enables you to use async logging for high-volume debug output while maintaining synchronous logging for critical audit trails that require immediate persistence.

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 →