# Thread-Safety Considerations for Using spdlog in Multi-Threaded Applications

> Learn essential thread-safety considerations for spdlog in multi-threaded apps. Discover how to use *_mt loggers, manage default loggers, and leverage async logging for better performance.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: best-practices
- Published: 2026-08-06

---

**Use `*_mt` loggers for concurrent access, avoid `set_default_logger()` during active logging, and prefer async loggers for high-contention scenarios.**

The spdlog logging library distinguishes between single-threaded and multi-threaded logger variants, with thread-safety guarantees enforced through internal mutexes, lock-free queues, and a thread-safe global registry. This guide explains how to configure thread-safe spdlog usage, what pitfalls to avoid, and how the source code implements these protections.

## Choosing Between `*_st` and `*_mt` Logger Variants

spdlog provides two naming conventions for every sink type:

- `*_st` (single-threaded): No internal locking; faster but unsafe for concurrent access
- `*_mt` (multi-threaded): Protected by `std::mutex`; safe for concurrent access from multiple threads

In [`include/spdlog/sinks/stdout_sinks.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/stdout_sinks.h), the `*_mt` sinks inherit from `std::mutex` or use a platform-specific `console_mutex` to serialize access to the underlying output stream. The `*_st` variants omit this mutex entirely.

Choose `*_mt` loggers when any thread might call logging methods simultaneously:

```cpp
// Safe for multi-threaded use
auto logger = spdlog::stdout_color_mt("console");
auto file_logger = spdlog::basic_logger_mt("file_logger", "app.log");

// Unsafe for multi-threaded use—will corrupt output under concurrency
auto unsafe = spdlog::stdout_color_st("console");

```

## Thread Safety of the Global Default Logger API

The convenience API (`spdlog::info()`, `spdlog::debug()`, etc.) forwards calls to a **default logger** stored in the global registry. As noted in [`include/spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/spdlog.h) (lines 129–132), this API is thread-safe **only when the underlying default logger is a `*_mt` instance**.

```cpp
// Safe: default logger is _mt, and no threads are active during setup
auto logger = spdlog::basic_logger_mt("default", "app.log");
spdlog::set_default_logger(logger);

// Now safe to call from any thread
spdlog::info("Thread-safe global logging");

```

### Critical Limitation: `set_default_logger()` Is Not Thread-Safe

The `set_default_logger()` function in [`spdlog.h`](https://github.com/gabime/spdlog/blob/main/spdlog.h) (lines 28–32) carries an explicit warning: replacing the default logger while other threads are logging causes data races and potential crashes. Perform this operation **before spawning worker threads** or guard it with external synchronization.

## The Thread-Safe Logger Registry

The global `spdlog::registry` class in [`include/spdlog/details/registry.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/registry.h) maintains a map of named logger instances. The header comment states "This class is thread safe," and the implementation protects all operations with an internal mutex.

Safe concurrent registry operations include:

- `spdlog::register_logger()`
- `spdlog::get(name)`
- `spdlog::drop(name)`

```cpp
// Thread-safe: multiple threads can retrieve loggers by name
auto logger = spdlog::get("console");  // Mutex-protected lookup
if (logger) {
    logger->info("Found existing logger");
}

```

## Async Loggers: Lock-Free Thread Safety

For high-concurrency workloads, **async loggers** eliminate per-call mutex contention. In [`src/async.cpp`](https://github.com/gabime/spdlog/blob/main/src/async.cpp), spdlog implements a lock-free MPMC (multi-producer, multi-consumer) queue where producer threads enqueue formatted log records and a dedicated thread pool handles sink I/O.

```cpp
// Initialize thread pool: queue size 8192, 1 background worker thread
spdlog::init_thread_pool(8192, 1);

// Create async file logger
auto async_logger = spdlog::basic_logger_mt<spdlog::async_factory>(
    "async", "logs/async.txt");

// Log from many threads without blocking on mutex
std::vector<std::thread> workers;
for (int i = 0; i < 16; ++i) {
    workers.emplace_back([i, async_logger] {
        for (int n = 0; n < 10000; ++n) {
            async_logger->info("High-volume message from thread {}", i);
        }
    });
}
for (auto& t : workers) t.join();

spdlog::shutdown();  // Flush remaining messages

```

### MDC Incompatibility with Async Mode

The **Mapped Diagnostic Context (MDC)** in [`include/spdlog/mdc.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/mdc.h) (lines 78–81) relies on thread-local storage to propagate diagnostic key-value pairs. As explicitly documented: *"MDC … not supported in asynchronous mode."*

With async loggers, log records may be processed by different threads than those that created them, breaking thread-local guarantees. Use MDC only with synchronous `*_mt` loggers.

## Performance Trade-offs and Contention Mitigation

| Approach | Synchronization Cost | Best For |
|----------|---------------------|----------|
| `*_st` logger | None | Single-threaded, maximum throughput |
| `*_mt` logger | Mutex per log call | Moderate concurrency, simple setup |
| Async logger | Lock-free enqueue, background I/O | High concurrency, bursty workloads |

When `*_mt` logger mutexes become a bottleneck, consider:

1. Switching to async loggers for lock-freeenqueue operations
2. Creating multiple independent loggers to distribute load across separate mutexes
3. Using `*_st` loggers with thread-local instances (one per thread, no sharing)

## Summary

- **Always use `*_mt` loggers** for concurrent access; `*_st` variants corrupt output when shared across threads
- **Global API thread safety** depends on the default logger being `*_mt`; `set_default_logger()` requires external synchronization or pre-thread setup
- **Registry operations** are mutex-protected and safe for concurrent registration and lookup
- **Async loggers** provide lock-free thread safety via MPMC queues but are incompatible with MDC and thread-local state
- **Replace default loggers before spawning threads**, never during active logging

## Frequently Asked Questions

### What happens if I use a `*_st` logger from multiple threads?

Output corruption occurs: log messages interleave unpredictably, buffers may overlap, and the application may crash due to race conditions on internal stream buffers. The `*_st` suffix explicitly means "single-threaded" with no internal synchronization.

### Is `spdlog::info()` thread-safe?

Yes, **if and only if** the current default logger is a `*_mt` or async logger installed before threads began logging. The global API itself contains no synchronization; it delegates to the underlying logger's thread-safety mechanisms.

### Can I safely create loggers from multiple threads?

Yes. The `spdlog::registry` class protects logger registration, retrieval, and deletion with a mutex, as documented in [`include/spdlog/details/registry.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/registry.h). Calls to `spdlog::register_logger()`, `spdlog::get()`, and `spdlog::drop()` are thread-safe.

### Why does async logging break MDC?

MDC stores diagnostic context in thread-local storage. With async loggers, the thread that formats and outputs the message is a background worker, not the thread that issued the log call. The worker thread cannot access the original thread's MDC values, rendering the feature ineffective.