# spdlog Sink Performance: A Complete Guide to Choosing Fast Logging Backends

> Explore spdlog sink performance differences. Learn how thread-safety, I/O cost, and processing impact logging speed and choose the fastest backend for your application.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: performance
- Published: 2026-08-06

---

**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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h). The critical path involves acquiring a mutex on every log call:

```cpp
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`](https://github.com/gabime/spdlog/blob/main/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:

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/basic_file_sink.h), [`include/spdlog/sinks/basic_file_sink-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/basic_file_sink-inl.h)

The fastest file sink performs a direct write without size checks:

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/rotating_file_sink.h), [`include/spdlog/sinks/rotating_file_sink-inl.h`](https://github.com/gabime/spdlog/blob/main/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):

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/daily_file_sink.h), [`include/spdlog/sinks/hourly_file_sink.h`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/stdout_sinks.h) | Writes via `fwrite` to `std::cout`; terminal driver is the bottleneck |
| **stdout_color_sink** | [`stdout_color_sinks.h`](https://github.com/gabime/spdlog/blob/main/stdout_color_sinks.h) | Adds ANSI color codes; minimal string processing overhead |
| **ansicolor_sink** | [`ansicolor_sink.h`](https://github.com/gabime/spdlog/blob/main/ansicolor_sink.h) | Windows console API calls add extra system calls for attribute setting |

Example color console usage:

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/null_sink.h)

The `sink_it_()` method is a no-op—no I/O, no allocation, no serialization:

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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:

```cpp
// 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`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h) — Mutex acquisition pattern for all sync sinks
- [`include/spdlog/async.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/async.h) — Thread pool and lock-free queue implementation (lines 31-55 for factory)
- [`include/spdlog/sinks/rotating_file_sink-inl.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/rotating_file_sink-inl.h) — Size check and `rotate_()` implementation (lines 16-63)
- [`include/spdlog/sinks/dup_filter_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/dup_filter_sink.h) — Hash map-based deduplication logic

## Summary

- **Lock contention** dominates synchronous sink performance—use `async_factory` for 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_mt` provides 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`](https://github.com/gabime/spdlog/blob/main/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.