# Using spdlog in Multithreaded Applications: Thread Safety and Performance Guide

> Discover how spdlog ensures thread safety and high performance in multithreaded applications. Learn to leverage mt loggers and asynchronous options for efficient logging. Optimize your code today.

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

---

**Yes, spdlog is fully thread-safe when using the `*_mt` (multi-threaded) synchronous loggers or the asynchronous logger backed by a dedicated thread pool.**

The `gabime/spdlog` library is specifically engineered for concurrent environments, offering two distinct architectures for multithreaded logging. Whether you need simple thread-safe console output or high-throughput lock-free logging, spdlog provides optimized implementations that eliminate data races without requiring external synchronization from your application code.

## Thread-Safe Logger Architectures

spdlog implements two primary logger families designed for concurrent access, each with different performance characteristics and use cases.

### Synchronous Multi-Threaded Loggers (*_mt)

The synchronous `*_mt` loggers provide **full thread safety** through internal mutex protection. When you call logging methods like `spdlog::info()` or `spdlog::error()`, the implementation acquires a lightweight lock, writes the formatted message to the sink, and releases the lock before returning.

According to the source in [`include/spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/spdlog.h), factory functions such as `spdlog::stdout_color_mt()` create loggers that are safe to use from any number of threads simultaneously. The underlying `spdlog::logger` class in [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h) implements this protection using a mutex that guards the sink vector and formatting operations.

### Asynchronous Loggers (async_logger)

For maximum throughput in hot paths, spdlog offers **lock-free asynchronous logging** via the `async_logger` class defined in [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h). This architecture decouples the logging thread from I/O operations:

- Producer threads enqueue formatted messages into a lock-free queue (no mutex acquisition)
- A background thread pool (implemented in [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h)) consumes the queue and writes to the sinks
- Log calls return immediately after enqueueing, making this ideal for latency-sensitive applications

## Implementation Details from Source Code

The thread-safety guarantees are enforced at the implementation level across several key files:

- **[`include/spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/spdlog.h)**: Contains the high-level API documentation noting that default loggers created via `*_mt` functions are thread-safe
- **[`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h)**: Implements the `std::mutex` protection for synchronous loggers in the `log()` method chain
- **[`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h)**: Defines `spdlog::init_thread_pool()` and the `async_logger` constructor that binds loggers to the global thread pool
- **[`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h)**: Implements the MPMC (multi-producer multi-consumer) queue and worker threads that process log messages without blocking application threads

## Practical Multithreaded Logging Examples

### Basic Thread-Safe Logging with Synchronous Loggers

Use this approach when you need immediate feedback and simple setup. The `*_mt` suffix ensures thread safety:

```cpp
#include <spdlog/spdlog.h>
#include <thread>
#include <vector>

void worker(int id) {
    spdlog::info("[thread {}] starting work", id);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    spdlog::info("[thread {}] finished work", id);
}

int main() {
    // Create a color, multi-threaded logger (thread-safe)
    auto logger = spdlog::stdout_color_mt("console");
    spdlog::set_default_logger(logger);
    spdlog::set_pattern("[%H:%M:%S.%e] [%t] %v");  // %t includes thread id

    const int n_threads = 4;
    std::vector<std::thread> threads;
    for (int i = 0; i < n_threads; ++i)
        threads.emplace_back(worker, i);

    for (auto &t : threads) t.join();
}

```

This example creates a `stdout_color_mt` logger where the `_mt` suffix indicates multi-threaded safety. The output will show interleaved log lines from all four workers, each prefixed with the thread ID captured via the `%t` pattern flag.

### High-Throughput Asynchronous Logging

For applications generating thousands of log events per second across many threads, use the asynchronous mode to eliminate lock contention:

```cpp
#include <spdlog/spdlog.h>
#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <thread>
#include <vector>

void busy_work(int id) {
    for (int i = 0; i < 1000; ++i) {
        spdlog::info("[thread {}] iteration {}", id, i);
    }
}

int main() {
    // Initialize global thread pool: 8KB queue, 2 background threads
    spdlog::init_thread_pool(8192, 2);

    auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("async.log", true);
    
    auto async_logger = std::make_shared<spdlog::async_logger>(
        "async_logger", 
        std::initializer_list<spdlog::sink_ptr>{sink},
        spdlog::thread_pool(), 
        spdlog::async_overflow_policy::block);

    spdlog::set_default_logger(async_logger);
    spdlog::set_pattern("[%H:%M:%S.%e] [%t] %v");

    const int n_threads = 8;
    std::vector<std::thread> workers;
    for (int i = 0; i < n_threads; ++i)
        workers.emplace_back(busy_work, i);

    for (auto &t : workers) t.join();
    
    spdlog::shutdown();  // Flush and stop background workers
}

```

The `async_overflow_policy::block` setting ensures that if the lock-free queue fills up (8192 slots in this example), producer threads will block rather than drop messages.

### Thread-Local Context with Mapped Diagnostic Context (MDC)

The **Mapped Diagnostic Context** allows you to attach per-thread key-value pairs to log output. However, as implemented in [`include/spdlog/mdc.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/mdc.h), this feature relies on thread-local storage and **only works with synchronous loggers**:

```cpp
#include <spdlog/spdlog.h>
#include <spdlog/mdc.h>
#include <thread>

void handler(int user_id) {
    spdlog::mdc::put("user", std::to_string(user_id));
    spdlog::info("handling request");
    spdlog::mdc::remove("user");
}

int main() {
    auto logger = spdlog::stdout_color_mt("console");
    spdlog::set_default_logger(logger);
    spdlog::set_pattern("[%H:%M:%S] [%t] %@ %v");  // %@ prints MDC content

    std::thread t1(handler, 42);
    std::thread t2(handler, 77);
    t1.join(); 
    t2.join();
}

```

Each thread maintains its own independent MDC map, ensuring that concurrent handlers do not contaminate each other's diagnostic context.

## Critical Considerations for Multithreaded Use

When deploying spdlog in concurrent applications, observe these constraints derived from the source implementation:

- **Never use `*_st` loggers in multithreaded code**: The single-threaded variants (e.g., `basic_logger_st`) deliberately omit mutex protection for maximum performance in single-threaded scenarios. Using them from multiple threads results in undefined behavior and data corruption.

- **Thread ID overhead**: When using the `%t` pattern flag to embed thread IDs, spdlog queries `std::this_thread::get_id()` on every log call. If you do not require thread identification, disable this feature by defining `SPDLOG_DISABLE_STD_THREAD` before including headers to reduce overhead.

- **MDC incompatibility with async mode**: Because `spdlog::mdc` uses thread-local storage, values set in the application thread will not propagate to the background thread pool used by async loggers. Use synchronous loggers exclusively when you need MDC functionality.

## Summary

- spdlog provides **two thread-safe architectures**: synchronous loggers with mutex protection (`*_mt`) and lock-free asynchronous loggers
- Use `spdlog::stdout_color_mt()` or other `*_mt` factory functions for immediate thread-safe logging without additional configuration
- For high-throughput scenarios, initialize a thread pool with `spdlog::init_thread_pool()` and create `async_logger` instances to eliminate producer-side locking
- Avoid single-threaded `*_st` variants entirely in multithreaded applications to prevent race conditions
- Mapped Diagnostic Context (`spdlog::mdc`) is available only with synchronous loggers due to its reliance on thread-local storage

## Frequently Asked Questions

### Is spdlog thread-safe by default?

Yes, if you use the `*_mt` (multi-threaded) factory functions such as `spdlog::stdout_color_mt()` or `spdlog::basic_logger_mt()`. These loggers protect all operations with an internal mutex. However, the `*_st` (single-threaded) variants are not thread-safe and must never be shared across threads.

### What is the difference between _mt and _st loggers?

The `_mt` suffix indicates that the logger uses a `std::mutex` to synchronize access to its sinks, making it safe for concurrent use from multiple threads. The `_st` suffix indicates a single-threaded optimization that omits this mutex for better performance, but which will cause data races if used concurrently. According to [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h), this distinction affects only the locking behavior; the API remains identical.

### Can I use Mapped Diagnostic Context (MDC) with async loggers?

No. The MDC implementation in [`include/spdlog/mdc.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/mdc.h) stores context data in thread-local storage (`thread_local`). Because async loggers process messages on a different thread pool than the one that generated them, the context data is not available during formatting. MDC only functions correctly with synchronous loggers.

### How do I achieve the highest logging throughput in a multithreaded application?

Use the asynchronous logger architecture. Initialize a global thread pool using `spdlog::init_thread_pool(queue_size, num_threads)`, then create an `async_logger` that shares this pool. This eliminates mutex contention on the logging thread by using a lock-free queue to pass messages to background workers, allowing your application threads to spend minimal time on logging operations.