# spdlog _st vs _mt Loggers: Thread Safety Explained

> Understand spdlog _st vs _mt loggers for thread safety. Choose _st for maximum throughput or _mt for safe concurrent access. Optimize your logging performance.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: deep-dive
- Published: 2026-07-25

---

**spdlog `_st` (single-threaded) loggers eliminate mutex overhead for maximum throughput in thread-confined scenarios, while `_mt` (multi-threaded) loggers provide built-in synchronization that is safe for concurrent access from multiple threads.**

The gabime/spdlog library provides two distinct logger factory families distinguished by `_st` and `_mt` suffixes, allowing developers to explicitly choose between raw performance and thread safety. Understanding spdlog thread safety is critical when designing concurrent logging architectures, as selecting the wrong variant can lead to data races or unnecessary synchronization overhead.

## Understanding spdlog Thread Safety Models

The distinction between single-threaded and multi-threaded loggers is reflected directly in the API naming convention and determines whether internal state is protected during concurrent access.

### Single-Threaded (_st) Loggers

**`_st`** loggers are optimized for scenarios where the caller guarantees that all logging operations originate from a single thread or that external synchronization is provided. In [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h), the core `logger` class defines the interface, while [`include/spdlog/logger-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger-inl.h) contains inline implementations that bypass mutex acquisition for `_st` variants.

Key characteristics:
- **No internal synchronization** – removes mutex overhead on every log call
- **Not thread-safe** – calling from multiple threads simultaneously results in undefined behavior
- **Higher throughput** – ideal for performance-critical, thread-confined components

### Multi-Threaded (_mt) Loggers

**`_mt`** loggers use a mutex to protect shared state, making them the default choice for general-purpose logging. According to the source in [`include/spdlog/logger-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger-inl.h), these implementations acquire locks to ensure that sink lists, formatting buffers, and internal data structures remain consistent during concurrent modifications.

Key characteristics:
- **Mutex-protected** – prevents message interleaving and data corruption
- **Thread-safe** – safe to call from any thread without external synchronization
- **Default recommendation** – the README explicitly warns: "only use if all your loggers are thread-safe (`_mt` loggers)"

## Performance vs. Safety Trade-offs

The primary reason for maintaining two variants is **performance**. The lock-free path of a single-threaded logger removes the overhead of mutex contention, yielding measurable speedups when logging from a dedicated worker thread. However, when several threads share a logger, the `_mt` variant guarantees that log messages are not interleaved and that internal structures remain valid.

Use an `_mt` logger unless you are absolutely sure that every log call originates from a single thread or you have implemented external synchronization. The performance penalty of `_mt` is modest compared to the cost of data races in multi-threaded environments.

## Creating spdlog _st and _mt Loggers

Factory functions append the suffix to indicate the thread-safety level. The underlying sink type determines the actual synchronization behavior.

### Multi-Threaded Logger Example

Use `stdout_color_mt` or `basic_logger_mt` for thread-safe console and file logging:

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

int main() {
    // Thread-safe logger that writes to the console with colors
    auto console = spdlog::stdout_color_mt("console");
    console->info("Hello from thread-safe logger");
    
    // Thread-safe file logger
    auto file_logger = spdlog::basic_logger_mt("file_logger", "app.log");
    file_logger->info("Safe for concurrent threads");
}

```

### Single-Threaded Logger Example

Use `basic_logger_st` when you control thread usage and need maximum speed:

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

void worker_thread() {
    // No internal locking – must be used only from this thread
    static auto logger = spdlog::basic_logger_st("worker", "worker.log");
    logger->debug("Worker step completed");
}

```

## Async Loggers and Thread Safety

Asynchronous loggers are built on top of the multi-threaded infrastructure but delegate actual message writing to a background thread pool. As defined in [`include/spdlog/async_logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async_logger.h), the `async_logger` itself is thread-safe at the API level, while the underlying sink (e.g., `basic_file_sink_mt`) follows the same `_mt`-vs-`_st` rule.

Note that the Mapped Diagnostic Context (MDC) currently works only with synchronous loggers because it relies on thread-local storage mechanisms that are incompatible with the async dispatch model.

```cpp
#include <spdlog/spdlog.h>
#include <spdlog/async.h>

int main() {
    // Must be called before any async logger creation
    spdlog::init_thread_pool(8192, 1); // queue size 8k, 1 background thread

    // Async logger built on top of a multi-threaded sink
    auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>(
        "async_file", "async.log");
    async_file->info("This goes through the async thread pool");
}

```

## When to Choose _st vs _mt Loggers

- **Choose `_mt`** for any logger that might be accessed from multiple threads, including global loggers or shared components. This is the safest default.
- **Choose `_st`** only when you deliberately instantiate a logger in a performance-critical, thread-confined component (e.g., a dedicated worker thread with no shared access) and need to minimize latency.

The [`README.md`](https://github.com/gabime/spdlog/blob/main/README.md) emphasizes that mixing `_st` loggers in multi-threaded contexts violates thread safety guarantees and can cause crashes or corrupted log output.

## Summary

- **`_mt` loggers** use internal mutex protection to ensure thread safety across concurrent calls, making them the appropriate default for most applications.
- **`_st` loggers** provide higher performance by eliminating synchronization overhead but are unsafe for use across multiple threads.
- **Async loggers** depend on the underlying sink's thread-safety type (`_mt` or `_st`) and offload I/O to a background thread pool while maintaining thread-safe APIs.
- **MDC features** are restricted to synchronous loggers due to thread-local storage dependencies.

## Frequently Asked Questions

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

**`_st`** (single-threaded) loggers lack internal mutex protection and are optimized for exclusive single-threaded use, while **`_mt`** (multi-threaded) loggers use locks to safely handle concurrent access from multiple threads. The `_mt` suffix appears in factory functions like `stdout_color_mt`, whereas `_st` appears in functions like `basic_logger_st`.

### Is spdlog thread-safe by default?

**Yes, if you use `_mt` factories.** The library provides both thread-safe and non-thread-safe variants, but you must explicitly choose the `_mt` suffix to obtain a thread-safe logger. The README warns against mixing `_st` loggers in multi-threaded applications.

### Can I use _st loggers in a multi-threaded application?

**Only if you provide external synchronization.** While an `_st` logger can exist within a multi-threaded process, calling its logging methods from multiple threads simultaneously without external locking causes data races. Restrict `_st` loggers to thread-confined scopes or protect access with your own mutex.

### Do async loggers require _mt sinks?

**Async loggers work with both types, but `_mt` sinks are recommended for thread safety.** The `async_logger` class handles thread-safe message queuing, but the underlying sink still follows the `_mt`/`_st` contract. If multiple async loggers share a sink, that sink must be `_mt` to prevent corruption during file writes.