# How Mapped Diagnostic Context (MDC) Works in spdlog: Thread-Local Logging Guide

> Learn how spdlog's Mapped Diagnostic Context (MDC) injects thread-local key-value pairs into your logs using the %MDC{key} pattern. Enhance your debugging with this powerful feature.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: deep-dive
- Published: 2026-07-14

---

**spdlog's Mapped Diagnostic Context (MDC) provides thread-local key-value storage that automatically injects diagnostic values into log messages via the `%MDC{key}` pattern placeholder.**

The Mapped Diagnostic Context (MDC) in spdlog enables automatic attachment of contextual metadata—such as request IDs or user sessions—to log entries without modifying every logging call. This lightweight facility stores data in thread-local storage, ensuring isolated contexts per thread with zero synchronization overhead.

## What is Mapped Diagnostic Context (MDC)?

Mapped Diagnostic Context is a diagnostic logging pattern that maintains a per-thread map of contextual data. In spdlog, the MDC implementation lives in `spdlog::detail::mdc` and is exposed through the public header [`include/spdlog/mdc.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/mdc.h). Unlike traditional logging where you must pass context variables explicitly to each log function, MDC allows you to set values once per thread and have them automatically appear in formatted output.

## Thread-Local Storage Architecture

The spdlog MDC implementation relies on **thread-local storage** to maintain independent context maps for each thread. According to the source code in [`include/spdlog/mdc.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/mdc.h), each thread maintains its own isolated map, eliminating the need for locks or synchronization primitives. This design ensures that context set in one thread never bleeds into another, making it ideal for multi-threaded server applications handling concurrent requests.

## Core MDC API Methods

The public API exposed in [`include/spdlog/mdc.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/mdc.h) provides four primary operations for managing thread-local context:

- **`spdlog::set_mdc(key, value)`** – Stores a string value associated with the specified key in the current thread's context map.
- **`spdlog::get_mdc(key)`** – Retrieves the value for a given key as an `optional<std::string>`, returning `std::nullopt` if the key does not exist.
- **`spdlog::remove_mdc(key)`** – Removes a specific key-value pair from the current thread's MDC map.
- **`spdlog::clear_mdc()`** – Clears all key-value pairs from the current thread's context, useful for cleanup at thread exit.

## Pattern Formatter Integration

The integration between MDC and log output occurs in [`include/spdlog/pattern_formatter.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/pattern_formatter.h). When your log pattern contains the `%MDC{key}` placeholder, the formatter queries the current thread's MDC map for that specific key during message formatting. If the key exists, its value replaces the placeholder; if absent, the placeholder expands to an empty string. This resolution happens automatically whenever a log message is formatted through the logger.

## Practical Code Examples

### Basic MDC Usage

The following example demonstrates setting, retrieving, and clearing MDC values with pattern formatting:

```cpp
#include <spdlog/spdlog.h>
#include <spdlog/mdc.h>

int main()
{
    // Create a logger (console sink for illustration)
    auto logger = spdlog::stdout_color_mt("example");

    // Define a pattern that prints the MDC entry "req_id"
    logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%MDC{req_id}] %v");

    // No MDC entry yet – placeholder will be empty
    logger->info("Started processing");

    // Set a request‑id for this thread
    spdlog::set_mdc("req_id", "42a7f9c3");

    logger->info("Handling request");           // → "[... ] [info] [42a7f9c3] Handling request"

    // Change the MDC value later on
    spdlog::set_mdc("req_id", "e9d2a1b0");
    logger->info("Finished request");           // → "[... ] [info] [e9d2a1b0] Finished request"

    // Remove a specific key
    spdlog::remove_mdc("req_id");
    logger->info("After removal");              // → "[... ] [info] [] After removal"

    // Clear the whole map (useful at thread exit)
    spdlog::clear_mdc();
}

```

### Multi-Threaded Server Implementation

In server applications, MDC eliminates the need to pass context parameters through every function call. Each thread maintains its own isolated context:

```cpp
#include <spdlog/spdlog.h>
#include <spdlog/mdc.h>
#include <thread>

void handle_client(int client_id)
{
    // Attach client‑specific context to this thread
    spdlog::set_mdc("client_id", std::to_string(client_id));

    auto logger = spdlog::get("server");
    logger->info("Connected");
    // …do work…
    logger->info("Disconnected");
}

int main()
{
    auto logger = spdlog::stdout_color_mt("server");
    logger->set_pattern("[%H:%M:%S] [client=%MDC{client_id}] %v");

    std::thread t1(handle_client, 1);
    std::thread t2(handle_client, 2);
    t1.join();
    t2.join();
}

```

Each thread prints its own `client_id` without any lock contention or cross-thread interference.

## Summary

- **Thread-local storage** in [`include/spdlog/mdc.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/mdc.h) ensures each thread maintains an independent MDC map with zero synchronization overhead.
- **Four core API functions**—`set_mdc`, `get_mdc`, `remove_mdc`, and `clear_mdc`—manage the lifecycle of context data.
- **Pattern integration** via `%MDC{key}` in [`include/spdlog/pattern_formatter.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/pattern_formatter.h) automatically injects context values into log messages.
- **Missing keys** result in empty string expansion, ensuring logs remain formatted even without context.
- **Automatic cleanup** using `clear_mdc()` prevents memory leaks when threads terminate.

## Frequently Asked Questions

### What happens if an MDC key is not set in spdlog?

When the pattern formatter encounters `%MDC{key}` for a key that does not exist in the current thread's MDC map, it expands the placeholder to an empty string. This behavior ensures log messages remain properly formatted even when optional context data is unavailable, as implemented in [`include/spdlog/pattern_formatter.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/pattern_formatter.h).

### Is MDC thread-safe in spdlog?

Yes, spdlog's MDC is inherently thread-safe because it utilizes thread-local storage. Each thread maintains its own isolated map in `spdlog::detail::mdc`, meaning no locks or mutexes are required when calling `set_mdc`, `get_mdc`, or `remove_mdc`. However, you must ensure that `clear_mdc()` is called before thread termination to prevent memory leaks.

### How do I prevent memory leaks when using MDC?

Always call `spdlog::clear_mdc()` at the end of a thread's lifecycle or when the context is no longer needed. This function clears all key-value pairs from the thread-local map, releasing stored strings. In long-running applications with dynamic thread pools, consider using RAII wrappers around `set_mdc` to ensure automatic cleanup via `clear_mdc()` or `remove_mdc()`.

### Can I use MDC with spdlog's asynchronous logging?

Yes, MDC works with asynchronous logging, but with important caveats. Since MDC is thread-local, the context must be set on the thread that calls the logging function. If you use asynchronous sinks where the formatting occurs on a different thread, the MDC context from the original calling thread will not automatically transfer. For async scenarios, either use synchronous logging or manually pass context through the message content itself.