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

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 (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 (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 queues messages into a ring buffer defined in include/spdlog/details/mpmc_blocking_q.h, which supports multiple producers safely.

The background processing relies on 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 (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 (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 (lines 129-130):

#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:

#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:

// 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.
  • 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 and background thread pools for high-throughput logging.
  • Registry protection: The global registry in 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 uses the lock-free queue defined in 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 (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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →