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

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

// 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 (lines 129–132), this API is thread-safe only when the underlying default logger is a *_mt instance.

// 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 (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 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)
// 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, 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.

// 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 (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. 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.

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 →