How to Configure spdlog's Periodic Flush Feature and Understand Thread-Safety Implications
Call spdlog::flush_every(std::chrono::duration) to start a background thread that flushes all registered loggers at fixed intervals; thread safety is guaranteed because every sink inherits from base_sink, which protects flush() with the same mutex used for log().
The spdlog library provides a periodic flush mechanism that automatically persists buffered log data without requiring manual logger->flush() calls. This feature spawns a dedicated background thread that repeatedly invokes a flush callback at user-defined intervals. Below is a complete guide to configuring this feature and understanding its multithreaded behavior based on the gabime/spdlog source code.
Enabling Periodic Flush with spdlog::flush_every()
The public API entry point is spdlog::flush_every() in include/spdlog/spdlog.h. This function forwards the request to the global registry singleton:
// include/spdlog/spdlog.h (lines 80-87)
template<typename Rep, typename Period>
inline void flush_every(std::chrono::duration<Rep, Period> interval)
{
details::registry::instance().flush_every(interval);
}
The registry stores the interval and creates a periodic_worker instance that encapsulates the background thread logic. To start flushing every 5 seconds:
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <chrono>
int main() {
auto file_logger = spdlog::basic_logger_mt("file_logger", "app.log");
// Start periodic flush: every 5 seconds
spdlog::flush_every(std::chrono::seconds(5));
// Normal logging operations...
}
How the Background Thread Works
The periodic_worker class in include/spdlog/details/periodic_worker.h manages the dedicated flush thread. Its implementation follows a standard condition variable pattern for periodic wakeups.
Thread Lifecycle
Construction starts the thread immediately:
// include/spdlog/details/periodic_worker-inl.h (lines 31-38)
SPDLOG_INLINE periodic_worker::periodic_worker(
const std::function<void()> &callback_fun,
interval_t interval)
{
if (interval == interval_t::zero()) { return; }
active_ = true;
worker_thread_ = std::thread([this, callback_fun, interval]() {
std::unique_lock<std::mutex> lock(mutex_);
while (active_) {
if (cv_.wait_for(lock, interval) == std::cv_status::timeout) {
callback_fun(); // Calls registry::flush_all()
}
}
});
}
Destruction signals termination and joins cleanly:
// include/spdlog/details/periodic_worker-inl.h (lines 14-22)
SPDLOG_INLINE periodic_worker::~periodic_worker()
{
if (worker_thread_.joinable()) {
{
std::lock_guard<std::mutex> lock(mutex_);
active_ = false;
}
cv_.notify_one();
worker_thread_.join();
}
}
The registry holds the worker in a std::unique_ptr<periodic_worker>, allowing safe reset when reconfiguring or shutting down.
Stopping or Reconfiguring the Flush Interval
To change the interval, simply call flush_every() again. The registry resets the worker and creates a new one with the updated duration:
// include/spdlog/details/registry.h (lines 71-74)
void registry::flush_every(interval_t interval)
{
std::lock_guard<std::mutex> lock(flusher_mutex_);
auto clbk = [this]() { this->flush_all(); };
periodic_flusher_ = details::make_unique<periodic_worker>(clbk, interval);
}
To stop periodic flushing entirely, pass a zero duration:
spdlog::flush_every(std::chrono::seconds(0)); // Stops and joins the thread
For complete cleanup at program exit, follow with spdlog::shutdown():
spdlog::flush_every(std::chrono::seconds(0));
spdlog::shutdown(); // Closes all loggers and releases resources
Thread-Safety Guarantees
The periodic flush mechanism is thread-safe by design through two layers of synchronization.
Sink-Level Mutex Protection
Every sink implementation inherits from base_sink<Mutex> (typically base_sink<std::mutex>). This base class protects both logging and flushing with the same mutex:
// include/spdlog/sinks/base_sink.h (lines 32-38)
void log(const details::log_msg &msg) override
{
std::lock_guard<Mutex> lock(sink_mutex_);
sink_it_(msg);
}
void flush() override
{
std::lock_guard<Mutex> lock(sink_mutex_);
flush_();
}
Because sink_mutex_ guards both sink_it_() (called during normal logging) and flush_() (called by the periodic worker), no race conditions exist between log writes and flush operations. The periodic flush simply blocks briefly until any concurrent log operation completes.
Logger and Registry Safety
The flush callback invokes registry::flush_all(), which iterates over registered loggers and calls each logger's flush_() method. In include/spdlog/logger-inl.h, this iterates over the logger's sinks and calls sink->flush() on each. Since all standard sinks derive from base_sink, each flush call acquires the appropriate lock automatically.
The periodic worker thread operates independently of user threads:
- It does not interfere with logger creation or destruction
- It does not block log formatting or queueing (only the final sink operation)
- It is safely joined during registry destruction or explicit reconfiguration
When to Use Periodic Flush
Recommended for:
- File-based sinks (
basic_file_sink,rotating_file_sink,daily_file_sink) that buffer OS-level writes - Custom network sinks that batch transmissions for efficiency
- Long-running applications where crash safety for recent logs is critical
Unnecessary for:
- Sinks with
force_flush = true(e.g.,stdout_sinkconfigured for immediate output) - Asynchronous loggers using the
async_loggerwith dedicated queues—these have their own flush mechanisms
Complete Working Example
#include <spdlog/spdlog.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <chrono>
int main() {
// Create rotating file sink: 5 MB size, 3 rotated files
auto rotating_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"app.log", 5 * 1024 * 1024, 3);
auto logger = std::make_shared<spdlog::logger>("main", rotating_sink);
spdlog::register_logger(logger);
// Flush every 10 seconds to balance performance and durability
spdlog::flush_every(std::chrono::seconds(10));
// Production logging loop
for (int i = 0; i < 10000; ++i) {
logger->info("Processing batch {} with {} items", i, i * 100);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// Graceful shutdown sequence
spdlog::flush_every(std::chrono::seconds(0)); // Stop periodic worker
spdlog::shutdown(); // Flush and close all sinks
return 0;
}
Comparison: Periodic Flush vs. Level-Based Flush
spdlog offers two complementary flush strategies:
| Mechanism | API | Trigger | Use Case |
|---|---|---|---|
| Periodic flush | spdlog::flush_every(duration) |
Time interval | Regular durability guarantees with minimal overhead |
| Level-based flush | spdlog::flush_on(spdlog::level::warn) |
Log level threshold | Immediate persistence for critical errors |
These can be combined: periodic flush for routine batches plus level-based flush for urgent messages.
Summary
- Enable periodic flush with
spdlog::flush_every(std::chrono::duration)—the background thread starts automatically viaperiodic_worker - Reconfigure by calling
flush_every()again with a new duration; stop with a zero duration - Thread safety is enforced through
base_sink'ssink_mutex_, which serializeslog()andflush()operations per sink - Cleanup requires explicit
flush_every(0s)followed byspdlog::shutdown()for orderly thread termination - Best suited for buffered sinks where durability must not depend on manual flush calls or program termination
Frequently Asked Questions
How do I completely disable periodic flushing once started?
Call spdlog::flush_every(std::chrono::seconds(0)). According to the periodic_worker constructor in include/spdlog/details/periodic_worker-inl.h line 34, a zero interval causes early return without thread creation. The registry's flush_every implementation (lines 71-74 in registry.h) resets the unique_ptr, invoking the destructor which joins any running thread.
Does periodic flush affect logging performance?
The impact is minimal and bounded. The periodic worker only contends for the sink_mutex during its brief flush window. Normal logging operations acquire the same mutex but release it immediately after the sink operation. The background thread sleeps on a condition variable between intervals, consuming no CPU.
Can I use periodic flush with asynchronous loggers?
Yes, but understand the interaction. Asynchronous loggers (async_logger) queue messages to a separate thread pool. The periodic flush still operates on the sinks, not the queue. For async loggers, you typically want spdlog::flush_every() to ensure sink buffers drain, while queue overflow is handled by the async worker's queue policy.
Why are my logs still missing after a crash with periodic flush?
The periodic flush guarantees data reaches the sink's flush operation, but OS-level buffering may still delay physical disk writes. For maximum durability, combine periodic flush with std::flush behavior in your sink configuration or use O_DSYNC/O_DIRECT file flags at the OS level.
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 →