# spdlog Backtrace Feature and Ring Buffer Usage: Implementation Guide

> Implement spdlog's backtrace feature with a ring buffer. Learn how to store and dump recent logs using spdlog::details::backtracer and circular_q. Enhance your debugging.

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

---

**spdlog's backtrace feature stores recent log messages in a lock-protected ring buffer and emits them on demand via `dump_backtrace()`, using `spdlog::details::backtracer` and `circular_q` components.**

The **gabime/spdlog** library provides a sophisticated mechanism to capture and replay recent log history through its backtrace feature, which utilizes a thread-safe ring buffer to temporarily store messages. This capability is essential for debugging difficult-to-reproduce failures by preserving contextual log messages leading up to an error. Understanding how spdlog implements this backtrace feature and ring buffer usage reveals the internal architecture of the `backtracer` and `circular_q` classes that enable this functionality.

## Core Architecture Components

### The backtracer Class

The `spdlog::details::backtracer` class, defined in [`include/spdlog/details/backtracer.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/backtracer.h), serves as the primary manager for the backtrace buffer. According to the source code, this class maintains three critical members:

- **`std::atomic<bool> enabled_`** (line 20): Controls whether message capture is active, allowing lock-free status checks
- **`std::mutex mutex_`** (line 19): Protects all buffer operations from concurrent access
- **`circular_q<log_msg_buffer> messages_`** (line 21): The ring buffer instance that stores log message copies

The class provides the `foreach_pop` routine that flushes stored messages to sinks when dumping the backtrace.

### The circular_q Ring Buffer Implementation

The `circular_q<T>` template in [`include/spdlog/details/circular_q.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/details/circular_q.h) implements the lock-free ring buffer mechanics used by the backtracer. Key implementation details include:

- **Allocation strategy**: The constructor (lines 28-31) allocates a vector sized `max_items_ + 1` to keep one slot as a "full" marker
- **Overwrite behavior**: When the queue reaches capacity, `push_back` (lines 45-55) silently overwrites the oldest entry
- **FIFO access**: Provides `front()`, `pop_front()`, `size()`, and `empty()` methods (lines 59-85) for ordered traversal

This generic circular queue is instantiated with `log_msg_buffer` to store formatted log messages.

## How Backtrace Capture Works

The backtrace feature operates through a four-phase lifecycle:

1. **Enable**: Calling `logger.enable_backtrace(N)` invokes `backtracer::enable`, which constructs a `circular_q<log_msg_buffer>` with capacity `N` (lines 30-34 of [`backtracer-inl.h`](https://github.com/gabime/spdlog/blob/main/backtracer-inl.h)) and sets `enabled_` to `true`

2. **Capture**: Every log call invokes `backtracer::push_back`, which wraps messages in `log_msg_buffer` objects and pushes them into the ring buffer. If the buffer is full, the oldest entry is dropped

3. **Dump**: When `logger.dump_backtrace()` is called, it invokes `backtracer::foreach_pop` (lines 53-61 of [`backtracer-inl.h`](https://github.com/gabime/spdlog/blob/main/backtracer-inl.h)), which iterates over the queue while holding the mutex, emitting "Backtrace Start" markers, each buffered message, and "Backtrace End" markers

4. **Disable**: Calling `logger.disable_backtrace()` clears the flag and releases the allocated buffer

## Thread Safety Mechanisms

All modifications to the backtrace buffer are guarded by the `mutex_` member, while the `enabled_` atomic flag allows concurrent enable/disable checks without locking. This design ensures that the `push_back` operation—which occurs on every log call—minimizes contention by only holding the lock during the brief ring buffer update. The `foreach_pop` method also acquires the mutex during the entire dump operation to ensure consistent message ordering.

## Usage Examples

### Basic Synchronous Backtrace

The following example demonstrates backtrace usage with a standard synchronous logger:

```cpp
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"

int main() {
    auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    spdlog::logger logger("example", console);

    logger.set_pattern("%v");           // only message text
    logger.enable_backtrace(5);         // keep last 5 messages

    logger.info("startup");             // printed immediately
    for (int i = 0; i < 100; ++i)
        logger.debug("debug {}", i);    // stored in backtrace buffer

    logger.dump_backtrace();            // prints backtrace block
}

```

### Asynchronous Logger Backtrace

The same API works with `spdlog::async_logger`, with the backtracer's internal mutex ensuring correct ordering even when the surrounding logger operates on a thread pool:

```cpp
#include "spdlog/async.h"
#include "spdlog/sinks/stdout_color_sinks.h"

int main() {
    spdlog::init_thread_pool(120, 1);               // 120 queue slots, 1 thread
    auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    auto async_logger = std::make_shared<spdlog::async_logger>(
        "async", console, spdlog::thread_pool());

    async_logger->set_pattern("%v");
    async_logger->enable_backtrace(5);

    async_logger->info("startup");
    for (int i = 0; i < 100; ++i)
        async_logger->debug("debug {}", i);

    async_logger->dump_backtrace();                 // flushed asynchronously
}

```

### Disabling Backtrace

To stop buffering and release resources:

```cpp
logger.disable_backtrace();   // stops buffering further messages

```

## Summary

- **spdlog's backtrace feature** uses a `circular_q` ring buffer to store recent log messages in memory
- **`spdlog::details::backtracer`** manages the buffer with a mutex-protected, atomic-enabled design for thread safety
- **Enable backtrace** by calling `logger.enable_backtrace(size_t)` to allocate the ring buffer with a specific capacity
- **Dump backtrace** by calling `logger.dump_backtrace()` to emit buffered messages between start/end markers
- **Works with both synchronous and asynchronous loggers**, with the internal mutex ensuring correct ordering across thread pools
- **Oldest messages are overwritten** when the ring buffer reaches capacity, implementing a true circular buffer behavior

## Frequently Asked Questions

### How do I enable backtrace in spdlog?

Call `logger.enable_backtrace(N)` where `N` is the number of messages to retain. This allocates a `circular_q` of that capacity and sets the atomic `enabled_` flag to true, causing subsequent log calls to push messages into the buffer while holding the internal mutex.

### Is spdlog backtrace thread-safe?

Yes. The `backtracer` class uses a `std::mutex` to protect all operations on the `circular_q` buffer, and an `std::atomic<bool>` for the enabled flag. This ensures that concurrent logging threads can safely push messages, and dump operations see a consistent view of the buffer.

### What happens when the backtrace buffer is full?

When the ring buffer reaches capacity, the `push_back` method in `circular_q` automatically overwrites the oldest entry (lines 45-55 of [`circular_q.h`](https://github.com/gabime/spdlog/blob/main/circular_q.h)). This silent overrun behavior ensures that the most recent `N` messages are always retained, which is typically the desired behavior for debugging the events leading to a crash.

### Can I use backtrace with asynchronous loggers?

Yes. The backtrace API works identically for `spdlog::async_logger` instances. The backtracer's internal mutex ensures correct ordering even when the surrounding logger operates on a thread pool with multiple worker threads, as verified in the test suite at [`tests/test_backtrace.cpp`](https://github.com/gabime/spdlog/blob/main/tests/test_backtrace.cpp).