# How to Create a Thread-Safe Custom Sink in spdlog

> Learn to create a thread-safe custom sink in spdlog by inheriting from base_sink and overriding sink_it_() and flush_(). Simplifies thread safety for custom logging.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: how-to-guide
- Published: 2026-07-22

---

**To create a thread-safe custom sink in spdlog, inherit from `spdlog::sinks::base_sink<std::mutex>` and override the protected virtual methods `sink_it_()` and `flush_()`; the base class handles all locking and formatter management automatically.**

The gabime/spdlog library isolates log output logic through a sink hierarchy centered on the `base_sink` template. When you need to send log messages to a custom destination—whether a network socket, database, or proprietary API—deriving from this template allows you to create a thread-safe custom sink in spdlog without implementing mutex logic yourself.

## Understanding the `base_sink` Template

The foundation of every thread-safe sink is the `base_sink<Mutex>` class defined in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h). This template implements the public `spdlog::sinks::sink` interface while handling the thread-safety glue internally.

The class holds two critical members:

- **`std::unique_ptr<formatter> formatter_`** – The pattern formatter shared with the logger
- **`Mutex mutex_`** – The synchronization primitive (template parameter)

When a log message arrives, the public `log()` method acquires `mutex_` and calls your implemented `sink_it_()`. Similarly, `flush()` locks before calling `flush_()`. This design ensures that your implementation code runs under exclusive lock without explicit synchronization code.

## Choosing the Mutex Type

The `Mutex` template parameter determines the thread-safety guarantee:

- **`std::mutex`** – Provides full thread-safe operation; use this for `*_mt` (multi-threaded) sink aliases
- **`spdlog::details::null_mutex`** – A zero-cost dummy mutex defined in [`include/spdlog/details/null_mutex.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/null_mutex.h); use this for `*_st` (single-threaded) sinks where you need no locking overhead

By instantiating your sink with `std::mutex`, you automatically satisfy spdlog's thread-safety requirements for concurrent logging.

## Step-by-Step Implementation

Follow these steps to implement your own thread-safe sink:

1. **Include the required headers** – You need [`spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/spdlog/sinks/base_sink.h) and optionally [`spdlog/details/null_mutex.h`](https://github.com/gabime/spdlog/blob/main/spdlog/details/null_mutex.h) for the single-threaded variant

2. **Derive from `base_sink<std::mutex>`** – This establishes the thread-safe behavior

3. **Override `sink_it_()`** – Format the `details::log_msg` using `formatter_->format()` and write to your destination

4. **Override `flush_()`** – Implement any destination-specific flush semantics

5. **Export type aliases** – Provide `my_sink_mt` and `my_sink_st` typedefs for convenience

## Complete Working Example

Below is a complete implementation that writes to a file while maintaining an in-memory ring buffer of recent messages:

```cpp
// my_custom_sink.h
#pragma once

#include <spdlog/sinks/base_sink.h>
#include <spdlog/details/null_mutex.h>
#include <spdlog/fmt/fmt.h>
#include <fstream>
#include <mutex>
#include <vector>

namespace spdlog {
namespace sinks {

template <class Mutex>
class my_custom_sink : public base_sink<Mutex>
{
public:
    explicit my_custom_sink(const std::string& filename, std::size_t cache = 100)
        : file_(filename, std::ios::app), cache_size_(cache) {}

protected:
    void sink_it_(const details::log_msg& msg) override
    {
        // Format message using the base class formatter
        memory_buf_t formatted;
        base_sink<Mutex>::formatter_->format(msg, formatted);

        // Write to file (executing under lock from base_sink)
        file_.write(formatted.data(), formatted.size());
        
        // Maintain ring buffer of recent lines
        const std::size_t eol_len = std::strlen(details::os::default_eol);
        if (lines_.size() < cache_size_)
        {
            lines_.emplace_back(formatted.begin(),
                                formatted.end() - static_cast<std::ptrdiff_t>(eol_len));
        }
        else
        {
            lines_.erase(lines_.begin());
            lines_.emplace_back(formatted.begin(),
                                formatted.end() - static_cast<std::ptrdiff_t>(eol_len));
        }
    }

    void flush_() override
    {
        file_.flush();
    }

private:
    std::ofstream file_;
    std::size_t cache_size_;
    std::vector<std::string> lines_;
};

// Type aliases for convenient instantiation
using my_custom_sink_mt = my_custom_sink<std::mutex>;
using my_custom_sink_st = my_custom_sink<details::null_mutex>;

} // namespace sinks
} // namespace spdlog

```

**Usage:**

```cpp
#include "my_custom_sink.h"
#include <spdlog/spdlog.h>

int main()
{
    auto sink = std::make_shared<spdlog::sinks::my_custom_sink_mt>("output.log", 50);
    auto logger = std::make_shared<spdlog::logger>("custom_logger", sink);
    spdlog::register_logger(logger);
    
    logger->info("Thread-safe logging to custom destination");
}

```

## Key Source Files in gabime/spdlog

For reference implementations and detailed interfaces, examine these files in the repository:

- **[`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h)** – Defines the abstract `sink` interface with virtual `log()`, `flush()`, and formatter methods
- **[`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h)** – Contains the `base_sink<Mutex>` template that implements the locking mechanism
- **[`include/spdlog/details/null_mutex.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/null_mutex.h)** – Provides the `null_mutex` type for zero-overhead single-threaded sinks
- **[`tests/test_sink.h`](https://github.com/gabime/spdlog/blob/main/tests/test_sink.h)** – Reference implementation showing `test_sink_mt` and `test_sink_st` used by the spdlog test suite

## Summary

- **Derive from `base_sink<std::mutex>`** to create thread-safe sinks; the base class automatically locks before calling your `sink_it_()` implementation
- **Override `sink_it_()` and `flush_()`** to implement destination-specific output logic; these methods run under the protection of the mutex
- **Use `formatter_->format(msg, buf)`** to apply the configured pattern to the raw log message
- **Provide `_mt` and `_st` aliases** using `std::mutex` and `null_mutex` respectively to follow spdlog naming conventions
- **Reference [`tests/test_sink.h`](https://github.com/gabime/spdlog/blob/main/tests/test_sink.h)** for a minimal working example of the pattern

## Frequently Asked Questions

### What is the difference between `_mt` and `_st` suffixes in spdlog sinks?

The `_mt` suffix indicates a thread-safe sink using `std::mutex`, while `_st` indicates a single-threaded sink using `spdlog::details::null_mutex`. The null_mutex provides zero-overhead locking for scenarios where the logger is only accessed from a single thread, eliminating synchronization costs entirely.

### Do I need to manually lock inside `sink_it_()` when using `base_sink`?

No. The `base_sink` template acquires the mutex in its public `log()` method before calling your protected `sink_it_()` implementation. Your code runs under exclusive lock automatically, so you should not acquire additional locks unless interfacing with external thread-unsafe resources that require their own synchronization.

### Can I use a custom mutex type instead of `std::mutex`?

Yes. As long as the type satisfies the BasicLockable requirements (provides `lock()` and `unlock()` methods), you can pass it as the template argument to `base_sink`. This allows integration with custom synchronization primitives, recursive mutexes, or instrumented locks for debugging.

### How do I access the formatted message string inside `sink_it_()`?

Call `base_sink<Mutex>::formatter_->format(msg, buf)` where `buf` is a `memory_buf_t` (typically `fmt::memory_buffer`). This populates the buffer with the formatted text according to the pattern set via `set_pattern()` or `set_formatter()`. Access the string content using `buf.data()` and `buf.size()` before writing to your destination.