How to Configure and Tune Async Logging with Thread Pool in spdlog
spdlog routes asynchronous log messages through a global lock-free queue serviced by dedicated worker threads, defaulting to 8192 queue items and one thread, which you can override via spdlog::init_thread_pool() before instantiating any async loggers.
The gabime/spdlog repository provides a high-performance C++ logging framework where all async loggers share a single global thread pool instance. Understanding how to configure this pool is essential for optimizing throughput, latency, and memory usage in production applications.
Understanding the Global Thread Pool Architecture
spdlog's asynchronous backend centers around the spdlog::details::thread_pool class defined in include/spdlog/details/thread_pool.h. This pool manages an mpmc_blocking_queue (multi-producer, multi-consumer) and a configurable number of worker threads that consume log messages from the queue and dispatch them to their final sinks.
The pool instance is stored as a shared pointer within the global registry (details::registry), accessible through methods in include/spdlog/details/registry.h and implemented in registry-inl.h. When you create an async logger, it obtains a weak reference to this pool via std::weak_ptr<details::thread_pool> thread_pool_ and forwards log entries using post_log or post_flush methods.
Without explicit configuration, the pool initializes lazily upon the first call to create an async logger, using constants defined in async.h: specifically details::default_async_q_size (8192 items) and a single worker thread.
Initializing the Thread Pool Explicitly
To customize queue capacity, thread count, or thread lifecycle callbacks, you must call spdlog::init_thread_pool() before constructing any async loggers. This function instantiates details::thread_pool and registers it via details::registry::instance().set_tp(...).
The library provides three overloads in include/spdlog/async.h:
init_thread_pool(size_t q_size, size_t thread_count)– basic configuration.init_thread_pool(size_t q_size, size_t thread_count, std::function<void()> on_thread_start)– with startup callback.init_thread_pool(size_t q_size, size_t thread_count, std::function<void()> on_thread_start, std::function<void()> on_thread_stop)– full control.
Default Lazy Initialization
If you skip explicit initialization, spdlog creates the pool automatically when you call create_async:
// Uses default: 8192 queue slots, 1 worker thread
auto logger = spdlog::create_async<spdlog::sinks::stdout_sink_mt>("default_async");
Custom Pool Configuration
For high-throughput scenarios, increase both queue size and thread count, and optionally pin threads to CPU cores:
// Initialize before any logger creation
spdlog::init_thread_pool(
/*queue_size*/ 16384,
/*thread_count*/ 4,
/*on_thread_start*/ []{
// Platform-specific: pin thread to core, set name, etc.
},
/*on_thread_stop*/ []{
// Cleanup logic when worker exits
}
);
// All subsequent async loggers use this configured pool
auto file_logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>(
"async_file", "logs/app.log");
Configuring Queue Size and Worker Threads
The two primary tuning parameters are queue size and thread count, both passed to the details::thread_pool constructor implemented in include/spdlog/details/thread_pool-inl.h.
- Queue size: Determines how many log messages can be buffered between producers and consumers. A larger queue absorbs burst traffic but consumes more memory. The default 8192 suits moderate loads; high-volume applications often require 65536 or higher.
- Thread count: Controls parallelism for sink operations. Since spdlog's thread pool primarily handles I/O-bound sink flushing, values between 1 and 4 typically maximize throughput. CPU-bound custom sinks may benefit from additional threads.
Handling Queue Overflow Policies
When producers outpace consumers, the queue fills. The async_overflow_policy enum in include/spdlog/async_logger.h (lines 21-27) defines three behaviors:
block(default): The calling thread waits until queue space is available. This ensures zero message loss but may stall producers.overrun_oldest: Discard the oldest message in the queue to make room for the new one. Useful for real-time systems where recent data matters more than historical logs.discard_new: Silently drop the incoming message if the queue is full.
You select the policy through the factory function used to create the logger:
// Blocking behavior (default)
auto blocking_logger = spdlog::create_async<spdlog::sinks::stdout_sink_mt>("blocking");
// Discard oldest when full
auto nb_logger = spdlog::create_async_nb<spdlog::sinks::stdout_sink_mt>("nonblock");
The create_async_nb helper instantiates an async logger with the non-blocking overrun policy, while create_async defaults to blocking.
Accessing the Pool at Runtime
After initialization, retrieve the current thread pool using spdlog::thread_pool(), which returns std::shared_ptr<details::thread_pool> from the registry's get_tp() method. This allows runtime introspection:
auto pool = spdlog::thread_pool();
std::cout << "Queue size limit: " << pool->queue_size() << '\n';
You can also check if the pool has been initialized by verifying whether the returned pointer is non-null before creating loggers.
Summary
- spdlog uses a single global
thread_pool(defined indetails/thread_pool.h) shared by all async loggers, stored in the global registry. - Default configuration provides 8192 queue items and one worker thread, initialized lazily on first async logger creation (
async.h). - Explicit initialization via
spdlog::init_thread_pool(q_size, thread_count, ...)must occur before any logger instantiation to customize capacity, parallelism, or thread callbacks. - Overflow behavior is controlled per-logger through
async_overflow_policy(block, overrun_oldest, discard_new) viacreate_asyncorcreate_async_nb. - Runtime access is available through
spdlog::thread_pool()for monitoring queue utilization.
Frequently Asked Questions
What is the default spdlog async thread pool size?
The default queue size is 8192 items (controlled by details::default_async_q_size in async.h) with one worker thread. This lazy-initialized configuration activates when you first call create_async without prior init_thread_pool() invocation.
When should I call init_thread_pool()?
You must call init_thread_pool() before creating any async loggers. Once the global thread pool is instantiated (either explicitly by you or implicitly by the first logger), subsequent calls to init_thread_pool() will not replace the existing pool, as the registry retains the first initialized instance.
How do I prevent log message loss when the queue is full?
Use the default blocking policy (async_overflow_policy::block) by creating loggers with spdlog::create_async. This causes producer threads to wait until space is available. Alternatively, increase the queue size via init_thread_pool() to accommodate traffic bursts, or implement application-level backpressure monitoring using spdlog::thread_pool()->queue_size().
Can I use multiple thread pools in the same application?
No. The spdlog architecture relies on a singleton global thread pool managed by details::registry. All async loggers share this single instance. If you need different overflow policies or resource isolation for different loggers, you must run them in separate processes or modify the library, as the registry pattern enforces one pool per process.
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 →