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_sink configured for immediate output)
  • Asynchronous loggers using the async_logger with 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 via periodic_worker
  • Reconfigure by calling flush_every() again with a new duration; stop with a zero duration
  • Thread safety is enforced through base_sink's sink_mutex_, which serializes log() and flush() operations per sink
  • Cleanup requires explicit flush_every(0s) followed by spdlog::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:

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 →