Using spdlog in Multithreaded Applications: Thread Safety and Performance Guide
Yes, spdlog is fully thread-safe when using the *_mt (multi-threaded) synchronous loggers or the asynchronous logger backed by a dedicated thread pool.
The gabime/spdlog library is specifically engineered for concurrent environments, offering two distinct architectures for multithreaded logging. Whether you need simple thread-safe console output or high-throughput lock-free logging, spdlog provides optimized implementations that eliminate data races without requiring external synchronization from your application code.
Thread-Safe Logger Architectures
spdlog implements two primary logger families designed for concurrent access, each with different performance characteristics and use cases.
Synchronous Multi-Threaded Loggers (*_mt)
The synchronous *_mt loggers provide full thread safety through internal mutex protection. When you call logging methods like spdlog::info() or spdlog::error(), the implementation acquires a lightweight lock, writes the formatted message to the sink, and releases the lock before returning.
According to the source in include/spdlog/spdlog.h, factory functions such as spdlog::stdout_color_mt() create loggers that are safe to use from any number of threads simultaneously. The underlying spdlog::logger class in include/spdlog/logger.h implements this protection using a mutex that guards the sink vector and formatting operations.
Asynchronous Loggers (async_logger)
For maximum throughput in hot paths, spdlog offers lock-free asynchronous logging via the async_logger class defined in include/spdlog/async.h. This architecture decouples the logging thread from I/O operations:
- Producer threads enqueue formatted messages into a lock-free queue (no mutex acquisition)
- A background thread pool (implemented in
include/spdlog/details/thread_pool.h) consumes the queue and writes to the sinks - Log calls return immediately after enqueueing, making this ideal for latency-sensitive applications
Implementation Details from Source Code
The thread-safety guarantees are enforced at the implementation level across several key files:
include/spdlog/spdlog.h: Contains the high-level API documentation noting that default loggers created via*_mtfunctions are thread-safeinclude/spdlog/logger.h: Implements thestd::mutexprotection for synchronous loggers in thelog()method chaininclude/spdlog/async.h: Definesspdlog::init_thread_pool()and theasync_loggerconstructor that binds loggers to the global thread poolinclude/spdlog/details/thread_pool.h: Implements the MPMC (multi-producer multi-consumer) queue and worker threads that process log messages without blocking application threads
Practical Multithreaded Logging Examples
Basic Thread-Safe Logging with Synchronous Loggers
Use this approach when you need immediate feedback and simple setup. The *_mt suffix ensures thread safety:
#include <spdlog/spdlog.h>
#include <thread>
#include <vector>
void worker(int id) {
spdlog::info("[thread {}] starting work", id);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
spdlog::info("[thread {}] finished work", id);
}
int main() {
// Create a color, multi-threaded logger (thread-safe)
auto logger = spdlog::stdout_color_mt("console");
spdlog::set_default_logger(logger);
spdlog::set_pattern("[%H:%M:%S.%e] [%t] %v"); // %t includes thread id
const int n_threads = 4;
std::vector<std::thread> threads;
for (int i = 0; i < n_threads; ++i)
threads.emplace_back(worker, i);
for (auto &t : threads) t.join();
}
This example creates a stdout_color_mt logger where the _mt suffix indicates multi-threaded safety. The output will show interleaved log lines from all four workers, each prefixed with the thread ID captured via the %t pattern flag.
High-Throughput Asynchronous Logging
For applications generating thousands of log events per second across many threads, use the asynchronous mode to eliminate lock contention:
#include <spdlog/spdlog.h>
#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <thread>
#include <vector>
void busy_work(int id) {
for (int i = 0; i < 1000; ++i) {
spdlog::info("[thread {}] iteration {}", id, i);
}
}
int main() {
// Initialize global thread pool: 8KB queue, 2 background threads
spdlog::init_thread_pool(8192, 2);
auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("async.log", true);
auto async_logger = std::make_shared<spdlog::async_logger>(
"async_logger",
std::initializer_list<spdlog::sink_ptr>{sink},
spdlog::thread_pool(),
spdlog::async_overflow_policy::block);
spdlog::set_default_logger(async_logger);
spdlog::set_pattern("[%H:%M:%S.%e] [%t] %v");
const int n_threads = 8;
std::vector<std::thread> workers;
for (int i = 0; i < n_threads; ++i)
workers.emplace_back(busy_work, i);
for (auto &t : workers) t.join();
spdlog::shutdown(); // Flush and stop background workers
}
The async_overflow_policy::block setting ensures that if the lock-free queue fills up (8192 slots in this example), producer threads will block rather than drop messages.
Thread-Local Context with Mapped Diagnostic Context (MDC)
The Mapped Diagnostic Context allows you to attach per-thread key-value pairs to log output. However, as implemented in include/spdlog/mdc.h, this feature relies on thread-local storage and only works with synchronous loggers:
#include <spdlog/spdlog.h>
#include <spdlog/mdc.h>
#include <thread>
void handler(int user_id) {
spdlog::mdc::put("user", std::to_string(user_id));
spdlog::info("handling request");
spdlog::mdc::remove("user");
}
int main() {
auto logger = spdlog::stdout_color_mt("console");
spdlog::set_default_logger(logger);
spdlog::set_pattern("[%H:%M:%S] [%t] %@ %v"); // %@ prints MDC content
std::thread t1(handler, 42);
std::thread t2(handler, 77);
t1.join();
t2.join();
}
Each thread maintains its own independent MDC map, ensuring that concurrent handlers do not contaminate each other's diagnostic context.
Critical Considerations for Multithreaded Use
When deploying spdlog in concurrent applications, observe these constraints derived from the source implementation:
-
Never use
*_stloggers in multithreaded code: The single-threaded variants (e.g.,basic_logger_st) deliberately omit mutex protection for maximum performance in single-threaded scenarios. Using them from multiple threads results in undefined behavior and data corruption. -
Thread ID overhead: When using the
%tpattern flag to embed thread IDs, spdlog queriesstd::this_thread::get_id()on every log call. If you do not require thread identification, disable this feature by definingSPDLOG_DISABLE_STD_THREADbefore including headers to reduce overhead. -
MDC incompatibility with async mode: Because
spdlog::mdcuses thread-local storage, values set in the application thread will not propagate to the background thread pool used by async loggers. Use synchronous loggers exclusively when you need MDC functionality.
Summary
- spdlog provides two thread-safe architectures: synchronous loggers with mutex protection (
*_mt) and lock-free asynchronous loggers - Use
spdlog::stdout_color_mt()or other*_mtfactory functions for immediate thread-safe logging without additional configuration - For high-throughput scenarios, initialize a thread pool with
spdlog::init_thread_pool()and createasync_loggerinstances to eliminate producer-side locking - Avoid single-threaded
*_stvariants entirely in multithreaded applications to prevent race conditions - Mapped Diagnostic Context (
spdlog::mdc) is available only with synchronous loggers due to its reliance on thread-local storage
Frequently Asked Questions
Is spdlog thread-safe by default?
Yes, if you use the *_mt (multi-threaded) factory functions such as spdlog::stdout_color_mt() or spdlog::basic_logger_mt(). These loggers protect all operations with an internal mutex. However, the *_st (single-threaded) variants are not thread-safe and must never be shared across threads.
What is the difference between _mt and _st loggers?
The _mt suffix indicates that the logger uses a std::mutex to synchronize access to its sinks, making it safe for concurrent use from multiple threads. The _st suffix indicates a single-threaded optimization that omits this mutex for better performance, but which will cause data races if used concurrently. According to include/spdlog/logger.h, this distinction affects only the locking behavior; the API remains identical.
Can I use Mapped Diagnostic Context (MDC) with async loggers?
No. The MDC implementation in include/spdlog/mdc.h stores context data in thread-local storage (thread_local). Because async loggers process messages on a different thread pool than the one that generated them, the context data is not available during formatting. MDC only functions correctly with synchronous loggers.
How do I achieve the highest logging throughput in a multithreaded application?
Use the asynchronous logger architecture. Initialize a global thread pool using spdlog::init_thread_pool(queue_size, num_threads), then create an async_logger that shares this pool. This eliminates mutex contention on the logging thread by using a lock-free queue to pass messages to background workers, allowing your application threads to spend minimal time on logging operations.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →