# Using spdlog in Embedded Systems: Configuration Guide for Microcontrollers

> Learn how to use spdlog in embedded systems. Configure for microcontrollers by disabling exceptions, using level filters, and implementing custom sinks for efficient logging.

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

---

**Yes, spdlog can be used in embedded systems by defining compile-time macros to disable exceptions and thread safety, utilizing level filters to strip logging code at compile time, and implementing lightweight custom sinks for hardware-specific output.**

The `gabime/spdlog` library is a header-only C++ logging framework designed with compile-time configuration options that make it adaptable to microcontroller environments. By leveraging preprocessor definitions found in [`include/spdlog/common.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/common.h) and the pluggable sink architecture defined in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h), you can deploy a type-safe logging interface on memory-constrained devices without requiring dynamic memory allocation or OS threading primitives.

## Essential Compile-Time Macros for Embedded Targets

Disabling heavy runtime features is the first step toward fitting spdlog into a firmware image. The library provides several macros in [`include/spdlog/common.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/common.h) that remove dependencies on OS-specific threading and exception handling.

### Removing Exception Handling

Embedded toolchains often disable C++ exceptions to save code space. Define `SPDLOG_NO_EXCEPTIONS` before including spdlog headers to replace `spdlog::spdlog_ex` throws with error codes or assertions:

```cpp
#define SPDLOG_NO_EXCEPTIONS
#include <spdlog/spdlog.h>

```

This macro ensures that error conditions in [`include/spdlog/common.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/common.h) use alternative reporting mechanisms compatible with `-fno-exceptions` compiler flags.

### Disabling Thread Safety Overhead

By default, spdlog protects log calls with `std::mutex` to support multi-threaded applications. For single-threaded firmware or bare-metal contexts, define `SPDLOG_DISABLE_THREAD_SAFETY` (available since v1.5) to compile out all mutex operations:

```cpp
#define SPDLOG_DISABLE_THREAD_SAFETY
#include <spdlog/spdlog.h>

```

Alternatively, use `SPDLOG_NO_TLS` to disable thread-local storage while retaining basic thread safety if your RTOS does not support TLS.

### Compile-Time Log Level Filtering

Control code size by setting the maximum log level at compile time. When you define `SPDLOG_LEVEL_INFO` or `SPDLOG_LEVEL_OFF` in [`include/spdlog/common.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/common.h), the preprocessor removes all lower-level logging calls entirely:

```cpp
#define SPDLOG_LEVEL_INFO  // Debug and trace calls are compiled out
#include <spdlog/spdlog.h>

```

Setting `SPDLOG_LEVEL_OFF` generates zero logging overhead, effectively removing all `SPDLOG_*` macros from the binary.

## Memory Management Strategies

Embedded systems often avoid dynamic heap allocation. While some standard sinks like `basic_file_sink` allocate buffers internally, you can configure spdlog to operate with static memory or minimal stack usage.

### Using the Null Sink

For size-optimized builds where logging is temporarily disabled or routed elsewhere, [`include/spdlog/sinks/null_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/null_sink.h) provides a sink that discards output without heap allocation:

```cpp
#include <spdlog/sinks/null_sink.h>

auto null_logger = std::make_shared<spdlog::logger>(
    "null", 
    std::make_shared<spdlog::sinks::null_sink_mt>()
);
spdlog::set_default_logger(null_logger);

```

### Stack-Based Formatting

By default, spdlog uses the `{fmt}` library for formatting. You can reduce heap usage by ensuring formatting occurs on the stack, or by compiling with `SPDLOG_USE_STD_FORMAT` disabled to keep formatting operations within static or stack memory bounds.

## Creating Custom Hardware Sinks

The pluggable sink interface in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h) allows you to route logs to hardware peripherals like UART, USB CDC, or CAN bus. Inherit from `spdlog::sinks::base_sink<Mutex>` and implement the pure virtual methods `sink_it_()` and `flush_()`.

### UART Sink Implementation

Below is a complete example for an ARM Cortex-M MCU using HAL drivers. This implementation disables exceptions and threading, then creates a UART sink that transmits formatted logs via blocking UART calls:

```cpp
#define SPDLOG_NO_EXCEPTIONS
#define SPDLOG_DISABLE_THREAD_SAFETY
#define SPDLOG_LEVEL_DEBUG

#include <spdlog/spdlog.h>
#include <spdlog/sinks/base_sink.h>
#include "uart_driver.h"  // Hardware-specific UART abstraction

template<typename Mutex>
class uart_sink : public spdlog::sinks::base_sink<Mutex>
{
public:
    explicit uart_sink(UART_HandleTypeDef* huart) : huart_(huart) {}

protected:
    void sink_it_(const spdlog::details::log_msg& msg) override
    {
        fmt::memory_buffer formatted;
        spdlog::sinks::base_sink<Mutex>::formatter_->format(msg, formatted);
        
        HAL_UART_Transmit(
            huart_, 
            reinterpret_cast<const uint8_t*>(formatted.data()),
            static_cast<uint16_t>(formatted.size()), 
            100
        );
    }

    void flush_() override { /* No buffering required for UART */ }

private:
    UART_HandleTypeDef* huart_;
};

int main()
{
    UART_HandleTypeDef huart1;  // Assume initialized by HAL
    
    auto uart = std::make_shared<uart_sink<std::mutex>>(&huart1);
    auto logger = std::make_shared<spdlog::logger>("uart", uart);
    spdlog::set_default_logger(logger);
    
    SPDLOG_DEBUG("Sensor reading: {:.2f}", 23.456);
}

```

When threading is disabled, you can substitute `std::mutex` with `spdlog::details::null_mutex` to eliminate lock overhead entirely.

## Build System Configuration

When integrating spdlog via CMake, set the relevant options in your [`CMakeLists.txt`](https://github.com/gabime/spdlog/blob/main/CMakeLists.txt) to ensure the macros propagate throughout your build:

```cmake
target_compile_definitions(your_firmware PRIVATE 
    SPDLOG_NO_EXCEPTIONS
    SPDLOG_DISABLE_THREAD_SAFETY
    SPDLOG_LEVEL_INFO
)

```

This approach ensures that all translation units respect the embedded constraints without requiring macro definitions in every source file.

## Summary

- **Define `SPDLOG_NO_EXCEPTIONS`** in [`include/spdlog/common.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/common.h) to remove C++ exception dependencies when using toolchains with `-fno-exceptions`.
- **Use `SPDLOG_DISABLE_THREAD_SAFETY`** to eliminate mutex overhead in single-threaded or bare-metal firmware.
- **Set `SPDLOG_LEVEL_*` macros** to strip logging calls at compile time, with `SPDLOG_LEVEL_OFF` generating zero overhead.
- **Implement custom sinks** inheriting from [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h) to redirect output to UART or other hardware peripherals without dynamic allocation.
- **Leverage header-only design** by including specific headers like [`include/spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/spdlog.h) and [`include/spdlog/sinks/null_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/null_sink.h) without linking external libraries.

## Frequently Asked Questions

### Can spdlog run on bare-metal microcontrollers without an RTOS?

Yes, spdlog can run on bare-metal systems by defining `SPDLOG_DISABLE_THREAD_SAFETY` to remove mutex dependencies and `SPDLOG_NO_EXCEPTIONS` to handle errors without C++ exceptions. You must provide a custom sink implementation that writes to your specific hardware interface, such as a UART peripheral.

### How do I reduce the flash size overhead of spdlog in embedded builds?

Define `SPDLOG_LEVEL_OFF` or `SPDLOG_LEVEL_INFO` before including headers to remove debug and trace logging calls at compile time. Additionally, use [`include/spdlog/sinks/null_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/null_sink.h) or minimal custom sinks that avoid formatting overhead when logging is disabled.

### Does spdlog require dynamic memory allocation on embedded targets?

While some standard sinks allocate heap memory, you can avoid dynamic allocation by implementing custom sinks that use static buffers or write directly to hardware registers. The `base_sink` interface in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h) supports stack-based formatting through `fmt::memory_buffer`, allowing zero-heap operation.

### What C++ standard library components does spdlog require?

spdlog requires basic STL headers including `<chrono>`, `<string>`, and `<vector>`. Most modern embedded toolchains (ARM GCC, Clang for embedded) provide these. If your environment lacks standard library support, you can define `SPDLOG_FMT_EXTERNAL` to use an external fmt library adapted to your platform, as referenced in [`include/spdlog/fmt.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/fmt.h).