# Thread Safety Considerations for Multi-Threaded spdlog Loggers: A Complete Guide

> Ensure thread safety with spdlog loggers in concurrent apps. Learn how atomic operations and lock-free queues guarantee safe logging, with exceptions to note.

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

---

**spdlog guarantees thread-safe logging for concurrent applications through atomic operations in the `spdlog::logger` class and lock-free queues for async loggers, though the `set_error_handler()` method remains the sole exception requiring external synchronization.**

The gabime/spdlog library is engineered for high-performance multi-threaded C++ applications, providing comprehensive thread safety mechanisms that vary between synchronous and asynchronous logging modes. Understanding these thread safety considerations ensures robust logging when multiple threads share logger instances across your application.

## Core Thread Safety Mechanisms

spdlog implements thread safety through distinct architectural patterns depending on whether you use synchronous or asynchronous loggers.

### Synchronous Logger Protection

The core `spdlog::logger` class defined in [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h) (lines 6-12) provides thread-safe operations for all public API methods except `set_error_handler()`. The implementation relies on two key mechanisms:

- **`should_log()`** reads an atomic `level_` variable to check logging thresholds without locking
- **`log_it_()`** acquires mutex locks only when necessary, with sinks maintaining independent formatter instances

As noted in [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h) (lines 14-15), each sink holds its own formatter, ensuring concurrent calls to the same logger do not share formatter state. This eliminates data races when formatting messages from different threads.

### Asynchronous Logging Architecture

For high-throughput scenarios, `spdlog::async_logger` uses a lock-free architecture. The implementation in [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) queues messages into a ring buffer defined in [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h), which supports multiple producers safely.

The background processing relies on [`include/spdlog/details/thread_pool.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/thread_pool.h), where the `post` method offers lock-free enqueue operations for producer threads. Worker threads execute a simple `while (queue.pop(...))` loop to process queued messages.

### Global Registry Safety

The global logger registry implemented in [`include/spdlog/details/registry.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/registry.h) (lines 9-10) is fully thread-safe, allowing loggers to be registered, retrieved, or removed from any thread without external synchronization.

## The Critical Exception: Error Handlers

While spdlog provides comprehensive thread safety, **`set_error_handler()`** is the only non-thread-safe method in the `spdlog::logger` class. You must configure error handlers before spawning any threads that might invoke the logger. Changing error handlers after threads begin logging creates a race condition on the handler function pointer.

## Compile-Time Thread Safety Configuration

spdlog provides macros in [`include/spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/spdlog.h) (lines 84-85) to customize thread safety at compile time:

- **`SPDLOG_NO_THREAD_SAFETY`**: Removes internal mutexes from all loggers, making them non-thread-safe but maximizing performance for single-threaded applications
- **`SPDLOG_DISABLE_DEFAULT_LOGGER`**: Disables the thread-safe default logger, leaving only a non-thread-safe "_st" variant

Only disable these protections when you can guarantee single-threaded access to logger instances.

## Practical Implementation Guidelines

Follow these guidelines when deploying spdlog in multi-threaded environments:

1. **Choose the appropriate logger type** for your concurrency needs. Use `spdlog::default_logger()` or standard `spdlog::logger` instances for multi-threaded access, or create async loggers using `spdlog::async_factory` when logging from many threads simultaneously.

2. **Initialize error handlers early**. Call `set_error_handler()` during application startup before any worker threads begin logging operations.

3. **Understand sink-level isolation**. While each sink formats its own copy of messages independently, remember that file I/O sinks rely on OS-level synchronization. Two threads logging identical messages will produce two independently formatted strings.

4. **Size async queues appropriately**. When creating async loggers with `spdlog::init_thread_pool(queue_size, thread_count)`, ensure the ring buffer accommodates burst traffic. If the queue fills, producer threads block until space becomes available.

5. **Consider performance trade-offs**. Async mode eliminates per-call mutex locking but introduces memory ordering costs from the lock-free queue. For extremely low-latency single-threaded logging, compile with `SPDLOG_NO_THREAD_SAFETY` and use the "_st" variants.

## Multi-Threaded Logging Examples

### Synchronous Thread-Safe Logging

The default logger accessed via `spdlog::default_logger()` is thread-safe as implemented in [`include/spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/spdlog.h) (lines 129-130):

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

void worker(int id) {
    auto logger = spdlog::default_logger();   // thread-safe logger
    logger->info("Worker {} started", id);
    // ... do work ...
    logger->info("Worker {} finished", id);
}

int main() {
    spdlog::set_level(spdlog::level::info);   // global log level
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    t1.join(); t2.join();
}

```

### Asynchronous Logging with Thread Pool

Create lock-free multi-threaded logging using the async factory:

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

int main() {
    // Create a thread-pool with 2 worker threads and a queue of 8192 messages
    spdlog::init_thread_pool(8192, 2);

    // Create an async logger that writes to a file
    auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>(
        "async_file", "logs/async.log");

    async_file->info("Async logger started");

    std::thread t1([&](){ async_file->info("Message from thread 1"); });
    std::thread t2([&](){ async_file->info("Message from thread 2"); });
    t1.join(); t2.join();

    async_file->flush();      // optional – forces queue drain
}

```

### Disabling Thread Safety for Single-Threaded Optimization

Compile with `-DSPDLOG_NO_THREAD_SAFETY` to remove synchronization overhead:

```cpp
// Compile with -DSPDLOG_NO_THREAD_SAFETY
#include <spdlog/spdlog.h>

int main() {
    // No internal mutex – fastest possible logging, but NOT safe across threads
    spdlog::info("Single-threaded fast logging");
}

```

## Summary

- **Default thread safety**: The `spdlog::logger` class protects all operations except `set_error_handler()` using atomic operations and mutexes, as defined in [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h).
- **Sink isolation**: Each sink maintains independent formatters, preventing shared state corruption during concurrent formatting operations.
- **Async architecture**: `spdlog::async_logger` uses lock-free queues in [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h) and background thread pools for high-throughput logging.
- **Registry protection**: The global registry in [`include/spdlog/details/registry.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/registry.h) ensures thread-safe logger registration and retrieval.
- **Compile-time options**: `SPDLOG_NO_THREAD_SAFETY` and `SPDLOG_DISABLE_DEFAULT_LOGGER` allow single-threaded optimizations when concurrent access is not required.

## Frequently Asked Questions

### Is spdlog thread-safe by default?

Yes, according to the gabime/spdlog source code, the default `spdlog::logger` and `spdlog::default_logger()` are thread-safe for all operations except `set_error_handler()`. The implementation uses atomic operations for log level checks and mutex protection for sink operations, ensuring safe concurrent access from multiple threads without external synchronization.

### What is the difference between synchronous and asynchronous thread safety in spdlog?

Synchronous loggers use mutex locks within the `log_it_()` method to protect sink writes, while asynchronous loggers eliminate per-call locking by delegating messages to a lock-free ring buffer processed by a background thread pool. The async approach in [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) uses the lock-free queue defined in [`include/spdlog/details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/mpmc_blocking_q.h), making it more scalable for high-contention scenarios with many producer threads.

### Can I disable thread safety to improve logging performance?

Yes, by defining `SPDLOG_NO_THREAD_SAFETY` before including spdlog headers or compiling with `-DSPDLOG_NO_THREAD_SAFETY`, you remove all internal mutexes from the logger classes. This compilation flag, referenced in [`include/spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/spdlog.h) (lines 84-85), creates non-thread-safe logger variants that offer maximum performance for single-threaded applications but will cause data races if accessed concurrently.

### Is it safe to share a single logger instance across multiple threads?

Absolutely. The `spdlog::logger` class is designed for shared use across threads, with each sink maintaining its own formatter instance to prevent cross-thread interference. However, you must call `set_error_handler()` before threads start, as this method is the only non-thread-safe operation in the logger API according to the source in [`include/spdlog/logger.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/logger.h).