# How to Configure and Tune Async Logging with Thread Pool in spdlog

> Learn to configure and tune async logging with thread pools in spdlog. Optimize your application's performance by customizing worker threads and queue sizes for efficient log management.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: how-to-guide
- Published: 2026-07-18

---

**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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/registry.h) and implemented in [`registry-inl.h`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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`:

```cpp
// 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:

```cpp
// 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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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:

```cpp
// 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:

```cpp
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 in [`details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/details/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`](https://github.com/gabime/spdlog/blob/main/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) via `create_async` or `create_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`](https://github.com/gabime/spdlog/blob/main/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.