How to Enable Asynchronous Logging with spdlog: A Complete Implementation Guide

To enable asynchronous logging with spdlog, initialize a global thread pool using spdlog::init_thread_pool(queue_size, n_threads), then instantiate an spdlog::async_logger that forwards log messages to a lock-free queue serviced by background worker threads rather than performing I/O on the calling thread.

Asynchronous logging prevents your application's hot path from stalling on slow disk or network operations. In the gabime/spdlog library, this is achieved through a dedicated thread pool and MPMC (multiple-producer, multiple-consumer) queue architecture defined in include/spdlog/async.h and implemented in src/async.cpp. This guide provides the exact implementation details based on the spdlog v1.x source code.

Understanding spdlog's Asynchronous Architecture

When operating in async mode, the spdlog::async_logger class (defined in include/spdlog/async_logger.h) overrides the standard logging behavior. Instead of immediately formatting and writing to sinks, it constructs a lightweight log_msg object and pushes it into a lock-free ring buffer. Background worker threads (managed in src/async.cpp) drain this queue, handle message formatting, and execute the actual I/O operations.

The core components include:

Step 1: Initialize the Global Thread Pool

You must create the thread pool before constructing any asynchronous loggers. This is a process-wide operation that launches the background worker threads.

#include <spdlog/async.h>

// Initialize with 8KB queue size and 1 background thread
spdlog::init_thread_pool(8192, 1);

The first parameter specifies the maximum number of pending messages in the queue. The second parameter defines how many worker threads will service the queue. According to src/async.cpp, this function instantiates a singleton thread pool that remains active for the process lifetime.

Step 2: Create an Asynchronous Logger

Once the thread pool exists, create loggers using either direct construction or the factory helper.

Direct Construction Method

For full control over sinks and policies, construct spdlog::async_logger explicitly:

#include <spdlog/async_logger.h>
#include <spdlog/sinks/basic_file_sink.h>

auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log", true);
auto async_logger = std::make_shared<spdlog::async_logger>(
    "async_file",                           // logger name
    spdlog::sinks_init_list{file_sink},    // sinks
    spdlog::thread_pool(),                  // thread pool reference
    spdlog::async_overflow_policy::block   // overflow policy
);

spdlog::register_logger(async_logger);

Factory Helper Method

For simplified creation, use the factory function provided in async.h:

auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto logger = spdlog::create_async_logger("my_async_logger", console_sink);

Step 3: Configure Overflow Policies

When the lock-free queue reaches capacity, spdlog applies an async overflow policy to determine how to handle new messages:

  • spdlog::async_overflow_policy::block (default): The calling thread blocks until queue space becomes available, ensuring no message loss.
  • spdlog::async_overflow_policy::overrun_oldest: The oldest queued message is discarded to accommodate the new one, guaranteeing the caller never blocks.

Set the policy globally before creating loggers:

spdlog::set_async_overflow_policy(spdlog::async_overflow_policy::overrun_oldest);

Complete Working Examples

Basic Async Logger with File Sink

This example demonstrates the full lifecycle from thread pool initialization to graceful shutdown:

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

int main() {
    // 1. Create thread pool: 1MB queue, 2 background threads
    spdlog::init_thread_pool(1024 * 1024, 2);
    
    // 2. Create file sink
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("async_log.txt", true);
    
    // 3. Construct async logger using default thread pool
    auto async_logger = std::make_shared<spdlog::async_logger>(
        "async_file_logger",
        spdlog::sinks_init_list{file_sink},
        spdlog::thread_pool(),
        spdlog::async_overflow_policy::block
    );
    
    // 4. Register for global access via spdlog::get()
    spdlog::register_logger(async_logger);
    
    // 5. Use logger - calls return immediately after enqueuing
    async_logger->info("Application started, version {}", 1.2);
    async_logger->warn("Low memory warning");
    async_logger->error("Failed to open {}", "config.json");
    
    // Ensure all queued messages are written before exit
    async_logger->flush();
}

High-Throughput Non-Blocking Configuration

For scenarios requiring maximum throughput without blocking:

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

int main() {
    // Smaller queue with overrun policy for fire-and-forget logging
    spdlog::init_thread_pool(4096, 1);
    spdlog::set_async_overflow_policy(spdlog::async_overflow_policy::overrun_oldest);
    
    auto logger = spdlog::create_async_logger("fast_logger",
        std::make_shared<spdlog::sinks::basic_file_sink_mt>("high_perf.log", true));
    
    // This loop never blocks, even if the consumer thread falls behind
    for (int i = 0; i < 1'000'000; ++i) {
        logger->info("High frequency message {}", i);
    }
}

Key Implementation Files in gabime/spdlog

The asynchronous logging system relies on these specific source files:

  • include/spdlog/async.h: Declares the public API including init_thread_pool() and overflow policy enums.
  • src/async.cpp: Implements the global thread pool, the MPMC queue management, and the worker_thread_func that runs in background threads.
  • include/spdlog/async_logger.h: Defines the async_logger class which inherits from spdlog::logger but enqueues messages rather than processing them immediately.
  • include/spdlog/details/mpmc_blocking_q.h: Contains the lock-free ring buffer implementation that enables efficient producer-consumer communication without mutex contention.

Summary

  • Thread pool prerequisite: Always call spdlog::init_thread_pool() before instantiating async loggers, as the pool must exist to service the message queue.
  • Lock-free architecture: The async_logger enqueues log_msg objects into an MPMC queue (include/spdlog/details/mpmc_blocking_q.h), decoupling your application from I/O latency.
  • Overflow handling: Choose between block (reliable delivery) and overrun_oldest (maximum performance) via set_async_overflow_policy().
  • API compatibility: Async loggers support the same sink chains and formatting patterns as synchronous loggers, requiring only the thread pool parameter for construction.
  • Resource management: Background threads are managed automatically by the singleton in src/async.cpp and persist until process termination.

Frequently Asked Questions

When should I use asynchronous logging with spdlog?

Use asynchronous logging when your application requires deterministic, low-latency performance and cannot tolerate stalls caused by slow I/O operations such as disk writes, network transmissions, or complex formatting. According to the gabime/spdlog source code, async mode ensures that log calls from your hot path perform only a lightweight enqueue operation on the lock-free queue, while background worker threads handle the expensive I/O operations.

What happens if the async queue fills up?

Behavior depends on the async overflow policy configured via spdlog::set_async_overflow_policy(). With the default block policy, the calling thread pauses until space is available in the queue. With overrun_oldest, the oldest pending message is discarded to make room for the new one. The queue size is fixed at initialization by the first parameter to init_thread_pool().

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

Yes. You can use both standard spdlog::logger instances (which perform I/O immediately on the calling thread) and spdlog::async_logger instances simultaneously. Only async loggers require the thread pool initialized via init_thread_pool(). This flexibility allows you to use synchronous loggers for critical error paths where you must confirm writes, and async loggers for high-volume debug logging.

How many threads should I allocate to the async thread pool?

For most applications, one thread is sufficient because logging is typically I/O bound rather than CPU bound. However, if you have multiple high-throughput loggers with CPU-intensive custom formatting or numerous slow sinks, increasing the thread count (the second parameter to init_thread_pool()) can improve throughput. Each thread runs spdlog::details::worker_thread_func and shares the single MPMC queue.

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 →