# spdlog Header-Only vs Compiled Library: Tradeoffs and Compile Time Analysis

> spdlog header-only vs compiled library: explore tradeoffs and compile times. Use header-only for prototyping, compiled for large projects to reduce build times and binary bloat.

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

---

**Use the header-only variant for rapid prototyping to eliminate linking steps, but choose the compiled library for large projects to minimize incremental build times and binary bloat.**

The [gabime/spdlog](https://github.com/gabime/spdlog) repository provides two distinct consumption models that share an identical API but differ significantly in build integration and compile-time performance. Understanding when to use each variant prevents unnecessary rebuild overhead in large codebases while maintaining flexibility for smaller utilities.

## Build System Architecture

The root [`CMakeLists.txt`](https://github.com/gabime/spdlog/blob/main/CMakeLists.txt) defines two mutually exclusive targets that determine how the library is consumed:

- **`spdlog_header_only`**: An interface target that adds `include/` directories and sets `SPDLOG_HEADER_ONLY`. No compiled artifacts are produced.
- **`spdlog`**: A static or shared library target compiled from sources in `src/`, producing `libspdlog.a` or equivalent.

Both targets handle the bundled `fmt` library dependency automatically, though `SPDLOG_FMT_EXTERNAL_HO` can force header-only fmt usage when needed.

## Compile Time Tradeoffs

### Header-Only Characteristics

When consuming spdlog as header-only, the compiler processes the full implementation in every translation unit that includes [`spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/spdlog/spdlog.h). In [`include/spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/spdlog.h), the code conditionally includes implementation files when `SPDLOG_HEADER_ONLY` is defined, effectively pasting the contents of [`src/spdlog.cpp`](https://github.com/gabime/spdlog/blob/main/src/spdlog.cpp) and related files directly into your object files.

This approach eliminates the linking step entirely, making initial project setup instantaneous. However, for projects with hundreds of translation units, this causes quadratic compilation overhead as the templated logging code is reinstantiated repeatedly.

### Compiled Library Benefits

The compiled variant builds the core implementation once into `libspdlog.a` (or `.so`/`.dll`), emitting object code only a single time. Translation units including spdlog headers see only declarations, reducing their individual compile time significantly. Incremental builds after modifying non-logging code become substantially faster, as the logging library does not require recompilation.

Binary size also benefits when linking the compiled library statically across multiple executables, as the linker can deduplicate the single library instance rather than merging redundant template instantiations from each translation unit.

## Runtime Performance and Binary Size

Despite the build-time differences, both variants generate **identical machine code** for logging operations. The header-only version is literally the same source that compiles into the library; no performance penalties or optimizations are sacrificed when switching between modes. The primary distinction lies in when and where compilation occurs, not what the processor executes.

## How to Choose Between Variants

**Select header-only when:**
- Prototyping single-file utilities or scripts where build simplicity outweighs compile speed
- Distributing header-only frameworks that cannot depend on external libraries
- Working in environments where running a linker is problematic (certain embedded toolchains)

**Select compiled library when:**
- Managing large codebases with many translation units
- Sharing logging code across multiple binaries via shared libraries
- Controlling symbol visibility and ABI boundaries explicitly
- Reducing CI build times where incremental compilation matters

## Implementation Details

The dual-mode mechanism relies on preprocessor conditionals in the public headers. The main entry point [`include/spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/spdlog.h) contains logic that either includes declarations alone or pulls in implementation headers from `include/spdlog/*/inlines.h` when `SPDLOG_HEADER_ONLY` is defined.

The compiled variant sources live in `src/`:
- [`src/spdlog.cpp`](https://github.com/gabime/spdlog/blob/main/src/spdlog.cpp): Core logger implementation
- [`src/fmt.cpp`](https://github.com/gabime/spdlog/blob/main/src/fmt.cpp): Bundled fmt library compilation (unless using external fmt)
- [`src/async.cpp`](https://github.com/gabime/spdlog/blob/main/src/async.cpp): Asynchronous logging support
- [`src/cfg.cpp`](https://github.com/gabime/spdlog/blob/main/src/cfg.cpp): Configuration parsing

When building the `spdlog` target, CMake compiles these `.cpp` files separately. When using `spdlog_header_only`, these same files are effectively `#include`d into every translation unit through the header structure.

## Practical CMake Integration

### Header-Only Consumption

```cmake
cmake_minimum_required(VERSION 3.12)
project(QuickDemo)

add_subdirectory(third_party/spdlog)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE spdlog_header_only)

```

```cpp
// main.cpp
#include "spdlog/spdlog.h"

int main() {
    spdlog::info("Zero-link compilation");
    // Compile: g++ -std=c++11 -I third_party/spdlog/include main.cpp
}

```

### Compiled Library Consumption

```cmake
cmake_minimum_required(VERSION 3.12)
project(ProductionApp)

add_subdirectory(third_party/spdlog)
add_executable(server server.cpp)
target_link_libraries(server PRIVATE spdlog)

```

```cpp
// server.cpp
#include "spdlog/spdlog.h"
#include "spdlog/sinks/rotating_file_sink.h"

int main() {
    auto logger = spdlog::rotating_logger_mt("file", "logs/app.log", 1048576*5, 3);
    logger->info("Using pre-compiled library");
}

```

## Summary

- **Header-only** (`spdlog_header_only` target) eliminates linking but recompiles implementation in every translation unit, ideal for small projects or prototypes.
- **Compiled library** (`spdlog` target) builds once and links, dramatically improving incremental build times for large codebases.
- Both variants share the same public API and runtime performance; switching requires only changing the CMake target name.
- The mechanism relies on `SPDLOG_HEADER_ONLY` controlling whether `src/` files are included directly or compiled separately.

## Frequently Asked Questions

### Does the header-only version produce slower runtime code than the compiled library?

No. The header-only variant generates identical machine code because it uses the same source files found in `src/`. The compiler emits the same instructions whether processing the code during your build or during the library's compilation.

### How do I switch from header-only to compiled library without changing my C++ code?

Change your CMake configuration from `target_link_libraries(your_target PRIVATE spdlog_header_only)` to `target_link_libraries(your_target PRIVATE spdlog)`. The public API remains identical, requiring no modifications to your `#include` directives or logging calls.

### Why are my compile times slow when using spdlog header-only in a large project?

Every `.cpp` file including [`spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/spdlog/spdlog.h) instantiates the full template-heavy implementation. In [`include/spdlog/spdlog.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/spdlog.h), this triggers compilation of formatting logic, registry management, and sink implementations per translation unit. Switch to the compiled `spdlog` target to compile these once and link them.

### Can I mix header-only and compiled spdlog usage in the same project?

No. Mixing modes causes One Definition Rule (ODR) violations because the header-only variant defines symbols that the compiled library also provides. Choose one consumption model per linked artifact, or use the compiled library as a shared object with careful symbol visibility control.