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:
include/spdlog/async.h: Exposesinit_thread_pool(),set_async_overflow_policy(), and factory functions.src/async.cpp: Implements the thread pool singleton and theworker_thread_funcloop that processes the queue.include/spdlog/details/mpmc_blocking_q.h: Contains the lock-free queue implementation used for inter-thread communication.
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 includinginit_thread_pool()and overflow policy enums.src/async.cpp: Implements the global thread pool, the MPMC queue management, and theworker_thread_functhat runs in background threads.include/spdlog/async_logger.h: Defines theasync_loggerclass which inherits fromspdlog::loggerbut 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_loggerenqueueslog_msgobjects 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) andoverrun_oldest(maximum performance) viaset_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.cppand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →