spdlog Sink Performance: A Complete Guide to Choosing Fast Logging Backends
The performance of spdlog sinks varies dramatically based on thread-safety model, I/O cost, and extra processing—ranging from zero-cost null_sink (no-op) to high-overhead rotating file sinks with mutex contention and filesystem operations.
Spdlog routes every log record through a sink, an object that determines where and how formatted messages are written. The sink implementation you choose directly impacts application throughput, latency, and scalability. This guide examines the performance implications of different spdlog sink implementations based on the actual source code in gabime/spdlog.
Thread-Safety Models: Synchronous vs. Asynchronous
Spdlog sinks fall into two architectural categories that fundamentally affect performance under concurrency.
Synchronous Sinks: Mutex-Per-Log
All synchronous sinks inherit from spdlog::sinks::base_sink<Mutex> in include/spdlog/sinks/base_sink.h. The critical path involves acquiring a mutex on every log call:
template <typename Mutex>
void base_sink<Mutex>::log(const details::log_msg &msg) {
std::lock_guard<Mutex> lock(mutex_);
sink_it_(msg);
}
This design serializes access to the underlying output resource. Contention increases linearly with thread count, making synchronous sinks unsuitable for high-frequency, multi-threaded logging without careful consideration.
Asynchronous Sinks: Lock-Free Queuing
The async_factory in include/spdlog/async.h creates loggers that enqueue messages in a lock-free ring buffer. A background thread pool dequeues and writes to the actual sinks:
#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>
auto logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>(
"async_file_logger", "logs/async.txt");
- Pros: Producer threads never block on sink mutexes; excellent scalability
- Cons: Added latency (async drain time); memory overhead for message queue
File Sink Performance in spdlog
File sinks vary significantly in overhead based on rotation logic and size checking.
basic_file_sink: Minimal Overhead
Source files: include/spdlog/sinks/basic_file_sink.h, include/spdlog/sinks/basic_file_sink-inl.h
The fastest file sink performs a direct write without size checks:
#include <spdlog/sinks/basic_file_sink.h>
auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/app.log");
file_logger->info("Synchronous file log – fast, no rotation");
In basic_file_sink-inl.h, the implementation calls file_helper_.write(formatted) directly—no additional processing between format and I/O.
rotating_file_sink: Rotation Cost
Source files: include/spdlog/sinks/rotating_file_sink.h, include/spdlog/sinks/rotating_file_sink-inl.h
Every write triggers a size check (new_size > max_size_). When rotation occurs, rotate_() performs filesystem renames and, on Windows, a sleep_for_millis(100) delay (lines 16-63):
#include <spdlog/sinks/rotating_file_sink.h>
auto rotating = spdlog::rotating_logger_mt(
"rot_logger", "logs/rot.log",
/*max size*/ 10 * 1024 * 1024,
/*max files*/ 5);
Performance impact: CPU and filesystem latency spikes at rotation boundaries. Use async wrapper to isolate this contention from producer threads.
Daily and Hourly File Sinks: Time-Based Overhead
Source files: include/spdlog/sinks/daily_file_sink.h, include/spdlog/sinks/hourly_file_sink.h
These sinks query the system clock on every write to detect rotation boundaries. The hourly variant incurs this cost more frequently than the daily variant. Both add small but measurable overhead compared to basic_file_sink.
Console Sink Performance Characteristics
Console output involves terminal drivers and system calls that dominate performance.
| Sink | Source File | Performance Notes |
|---|---|---|
| stdout_sink | stdout_sinks.h |
Writes via fwrite to std::cout; terminal driver is the bottleneck |
| stdout_color_sink | stdout_color_sinks.h |
Adds ANSI color codes; minimal string processing overhead |
| ansicolor_sink | ansicolor_sink.h |
Windows console API calls add extra system calls for attribute setting |
Example color console usage:
#include <spdlog/sinks/stdout_color_sinks.h>
auto console = spdlog::stdout_color_mt("console");
console->error("Error printed in red on the terminal");
Console I/O is inherently slower than file I/O due to terminal rendering, screen buffer management, and synchronized console access on Windows.
Specialized Sinks: Eliminating or Transforming Work
null_sink: Zero-Cost Logging
Source file: include/spdlog/sinks/null_sink.h
The sink_it_() method is a no-op—no I/O, no allocation, no serialization:
#include <spdlog/sinks/null_sink.h>
auto silent = std::make_shared<spdlog::sinks::null_sink_mt>();
auto logger = std::make_shared<spdlog::logger>("null_logger", silent);
Use null_sink for maximum throughput benchmarking or when you need to compile out logging in performance-critical paths without removing call sites.
dup_filter_sink: Deduplication Trade-off
Source file: include/spdlog/sinks/dup_filter_sink.h
This sink maintains a hash map of recent messages to suppress duplicates. The overhead:
- Hash computation and map lookup per log call
- Memory allocation for map entries
- Cache pressure from map traversal
Acceptable when deduplication dramatically reduces downstream I/O volume.
dist_sink: Multi-Destination Fan-out
Source file: include/spdlog/sinks/dist_sink.h
Forwards each message to multiple child sinks. Performance equals the sum of all child sink costs plus dispatch overhead. Combine strategically:
// Fast path: null_sink + backup file
dist_sink->add_sink(std::make_shared<null_sink_mt>()); // Discard
dist_sink->add_sink(std::make_shared<basic_file_sink_mt>(path)); // Preserve
Performance-Guided Sink Selection Matrix
| Use Case | Recommended Sink | Rationale |
|---|---|---|
| Maximum throughput, benchmarking | null_sink |
No I/O, no locks, no allocation |
| High-frequency production logging | basic_file_sink_mt + async factory |
Lock-free queuing eliminates contention; minimal per-message work |
| Rotating logs with many threads | rotating_file_sink_mt + async factory |
Rotation cost isolated to background thread |
| Human-readable development output | stdout_color_sink_mt |
Console I/O bottleneck dominates; simplicity preferred |
| Deduplication of noisy errors | dup_filter_sink_mt wrapping file sink |
Hash overhead justified by reduced write volume |
| Selective multi-destination logging | dist_sink_mt with null_sink + basic_file_sink_mt |
Discard non-critical, preserve errors cheaply |
Implementation Details from Source Code
Key performance-relevant files in gabime/spdlog:
include/spdlog/sinks/base_sink.h— Mutex acquisition pattern for all sync sinksinclude/spdlog/async.h— Thread pool and lock-free queue implementation (lines 31-55 for factory)include/spdlog/sinks/rotating_file_sink-inl.h— Size check androtate_()implementation (lines 16-63)include/spdlog/sinks/dup_filter_sink.h— Hash map-based deduplication logic
Summary
- Lock contention dominates synchronous sink performance—use
async_factoryfor multi-threaded, high-frequency logging - I/O cost hierarchy:
null_sink(none) < file sinks < console sinks (highest) - Feature overhead: Rotation, filtering, and distribution add measurable per-message cost; enable only when functionally required
- Optimal combination: Async logger with
basic_file_sink_mtprovides excellent throughput for production workloads
Frequently Asked Questions
How much faster is null_sink compared to file sinks?
null_sink eliminates all I/O and most processing overhead. In throughput benchmarks, it typically achieves 10-100x higher message rates than file sinks and 100-1000x higher than console sinks, making it ideal for measuring formatting overhead independent of I/O.
Should I always use async loggers for multi-threaded applications?
Not always. Async loggers add latency (messages are not immediately written) and memory overhead for the message queue. For moderate logging frequencies with few threads, synchronous basic_file_sink_mt may suffice. Use async when you observe mutex contention or require maximum producer thread throughput.
Why does rotating_file_sink have a 100ms sleep on Windows?
The sleep_for_millis(100) in rotating_file_sink-inl.h (line 63) ensures filesystem consistency during rename operations on Windows, where file handles may not release immediately. This adds latency spikes during rotation but prevents data corruption.
Can I combine multiple sinks without linear performance degradation?
With dist_sink, overhead is additive. However, you can optimize by placing cheap sinks (like null_sink) before expensive ones and using dup_filter_sink to skip redundant work. Structure the sink chain so that expensive operations are gated by cheaper filters.
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 →