# Thread-Safety Guarantees of spdlog Loggers: A Complete Technical Guide

> Discover spdlog thread-safety guarantees. Learn how synchronous and asynchronous loggers ensure safe concurrent logging with atomic variables, mutexes, or lock-free queues. Get the complete technical guide.

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

---

**spdlog loggers are thread-safe by default, with synchronous loggers protecting operations via atomic variables and internal mutexes, while asynchronous variants use lock-free queues to eliminate contention between producer threads and I/O operations.**

The **gabime/spdlog** library is designed for concurrent environments, providing robust **thread-safety guarantees** that eliminate data races without requiring manual synchronization in user code. Whether you use the default synchronous logger or the high-throughput asynchronous variant, understanding the specific safety boundaries and compile-time options ensures optimal performance in multi-threaded C++ applications.

## Core Thread-Safety Guarantees in spdlog

### The Default Logger (spdlog::default_logger)

The global default logger returned by `spdlog::default_logger()` is fully **thread-safe** for all logging operations. According to the source code in [[`spdlog.h`](https://github.com/gabime/spdlog/blob/main/spdlog.h) line 129‑130](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/spdlog.h#L129-L130), this logger instance protects its internal state using atomic operations and mutexes, allowing multiple threads to call logging methods concurrently without external locking.

### Synchronous Logger (spdlog::logger)

The core `spdlog::logger` class guarantees thread safety through two key mechanisms defined in [[`logger.h`](https://github.com/gabime/spdlog/blob/main/logger.h) line 6‑12](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/logger.h#L6-L12):

- **`should_log()`** reads from an atomic `level_` variable, ensuring lock-free level checks
- **`log_it_()`** acquires a mutex only when formatting and sinking messages, minimizing critical section duration

**Critical exception:** The `set_error_handler()` method is **not thread-safe**. You must configure error handlers before spawning threads that use the logger.

### Sink-Level Safety

Each sink maintains its own formatter instance, as implemented in [[`logger.h`](https://github.com/gabime/spdlog/blob/main/logger.h) line 14‑15](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/logger.h#L14-L15). This design eliminates shared mutable state during message formatting, meaning concurrent calls to the same logger produce independent formatted strings rather than racing on a shared buffer.

## Asynchronous Logging and Thread Safety

### Lock-Free Queue Implementation

The `spdlog::async_logger` variant delegates I/O to a background thread pool, using a **lock-free multi-producer/multi-consumer queue** defined in [[`details/mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/details/mpmc_blocking_q.h)](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/details/mpmc_blocking_q.h). This architecture, exposed through [[`async.h`](https://github.com/gabime/spdlog/blob/main/async.h)](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/async.h), allows multiple threads to enqueue log messages without mutex contention.

### Thread Pool Architecture

The [[`thread_pool.h`](https://github.com/gabime/spdlog/blob/main/thread_pool.h)](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/details/thread_pool.h) implementation provides dedicated worker threads that consume from the lock-free queue. Producer threads calling `post()` experience no blocking except when the queue reaches capacity, while background workers handle all file I/O and formatting serially per sink.

## Global Registry Thread Safety

The singleton registry managing all logger instances (`spdlog::details::registry`) is thread-safe for registration, retrieval, and removal operations. As noted in [[`registry.h`](https://github.com/gabime/spdlog/blob/main/registry.h) line 9‑10](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/details/registry.h#L9-L10), you may safely create or access named loggers from any thread without external synchronization.

## Compile-Time Options Affecting Thread Safety

You can disable thread-safety mechanisms via preprocessor definitions in [[`spdlog.h`](https://github.com/gabime/spdlog/blob/main/spdlog.h) line 84‑85](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/spdlog.h#L84-L85):

- **`SPDLOG_NO_THREAD_SAFETY`** – Removes internal mutexes from all loggers, creating single-threaded "_st" variants that offer maximum performance but **no concurrent access protection**
- **`SPDLOG_DISABLE_DEFAULT_LOGGER`** – Eliminates the thread-safe default logger, requiring explicit logger creation

Only define these macros when you can guarantee single-threaded access or implement your own external synchronization.

## Practical Implementation Examples

### Basic Thread-Safe Synchronous Logging

```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 Logger with Custom Thread Pool

```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 Performance

```cpp
// Compile with -DSPDLOG_NO_THREAD_SAFETY or define SPDLOG_NO_THREAD_SAFETY
// before including spdlog headers.

#include <spdlog/spdlog.h>

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

```

## Summary

- **Synchronous loggers** protect all operations except `set_error_handler()` using atomic level checks and per-logger mutexes
- **Asynchronous loggers** eliminate producer-side locking via lock-free queues in [`mpmc_blocking_q.h`](https://github.com/gabime/spdlog/blob/main/mpmc_blocking_q.h), delegating I/O to a background `thread_pool`
- **Sinks** maintain independent formatter instances, preventing data races during message formatting
- The **global registry** allows thread-safe logger registration and lookup from any thread
- Compile-time flags (`SPDLOG_NO_THREAD_SAFETY`) can remove synchronization overhead for single-threaded applications

## Frequently Asked Questions

### Is spdlog::logger thread-safe?

Yes, the `spdlog::logger` class is thread-safe for all logging operations. The `should_log()` method reads from an atomic `level_` variable, while `log_it_()` acquires a mutex only when necessary. The only exception is `set_error_handler()`, which must be called before threads begin logging.

### Do I need to lock spdlog loggers manually?

No, you do not need external mutexes when using spdlog's default configuration. Both the synchronous `spdlog::logger` and asynchronous `spdlog::async_logger` handle internal synchronization automatically. Only when compiling with `SPDLOG_NO_THREAD_SAFETY` must you provide your own synchronization or restrict access to a single thread.

### Is the async logger faster than the synchronous logger?

For high-contention scenarios with many producer threads, the async logger typically provides better throughput because it uses a lock-free queue to decouple logging calls from disk I/O. However, for single-threaded applications or extremely low-latency requirements, the synchronous logger (especially the "_st" variant) may offer lower individual call latency due to avoiding queue overhead.

### What happens if I call set_error_handler from multiple threads?

Calling `set_error_handler()` concurrently from multiple threads results in undefined behavior, as this is the only non-thread-safe method in the `logger` class. You must set the error handler immediately after creating the logger and before any other threads access it.