Thread-Safety Guarantees of spdlog Loggers: A Complete Technical Guide
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 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 line 6‑12](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/logger.h#L6-L12):
should_log()reads from an atomiclevel_variable, ensuring lock-free level checkslog_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 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/v1.x/include/spdlog/details/mpmc_blocking_q.h). This architecture, exposed through [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/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 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 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 protectionSPDLOG_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
#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
#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
// 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, delegating I/O to a backgroundthread_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.
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 →