Why Your Zephyr Build Takes 5 Minutes and How to Speed It Up

Speeding up slow Zephyr RTOS build times with smarter CMake and incremental compilation

You hit save. You hit west build. The spinner starts. You glance at Slack. Twenty minutes later you’re three threads deep in a debate about TypeScript, and the build finished 14 minutes ago.

If you do 30 builds a day and 4 minutes of each one is wasted, that’s 2 hours of focus shredded daily. Across a team of 5, that’s a full engineer’s week every week.

Most “zephyr build slow” complaints aren’t really about compilation. They’re about CMake reconfigures and full rebuilds masquerading as incremental ones. Below is a profiling method, then 5 fixes ordered by ROI. A well-tuned Zephyr inner loop should be sub-30 seconds for a single-file C change. If yours isn’t, something on this list will fix it.

Profile Before You Optimize

Measure where the zephyr build time actually goes.

Three phases to time separately:

  1. CMake configure (parsing CMakeLists, devicetree, Kconfig)
  2. Ninja graph generation (dependency analysis)
  3. Compile and link

To see what CMake is doing:

west build -- --trace-expand --profiling-format=google-trace --profiling-output=cmake.trace

Drop cmake.trace into chrome://tracing and you’ll see exactly which CMake calls eat your seconds.

For Ninja, run this in your build directory after a build:

ninja -d stats

It tells you how many edges actually ran versus how many were up-to-date. If your “zephyr incremental build” rebuilt 800 files because you changed one header, you have a header-dependency problem, not a Ninja problem.

Here’s roughly what a healthy breakdown looks like:

Phase              Cold Build    Incremental (typical)
-----------------  -----------   ---------------------
CMake configure    20-60s        0s (should be)
DT + Kconfig gen   10-30s        0-5s
Compile            60-180s       2-15s
Link               5-20s         5-20s
-----------------  -----------   ---------------------
Total              ~5 min        target: <30s

If your incremental build re-runs CMake configure, that’s the bug. Find out why before touching anything else. Common causes: a CMakeLists.txt touched by your editor’s “format on save,” a prj.conf edit, a west update, or a branch switch that updated a manifest file.

Five Fixes, Ordered by ROI

Fix 1: Stop Triggering CMake Reconfigures

This is the single biggest lever for zephyr cmake speed. A reconfigure adds 30 to 90 seconds to every “incremental” build, and most of them are self-inflicted.

Triggers to watch for:

  • Editing CMakeLists.txt, prj.conf, or any .overlay will invalidate the cache immediately
  • west update (touches modules, invalidates the cache)
  • Branch switches that change any of the above
  • Passing --pristine or -p always

--pristine is the worst offender. People put it in shell aliases “just to be safe” and pay the tax forever. Reserve it for clean baselines and release builds. For your dev loop:

west build -b nrf52840dk/nrf52840 app

No -p. Ever.

For Kconfig changes, run west build -t menuconfig and save. Zephyr will regenerate only what’s needed instead of forcing a full reconfigure from editing prj.conf by hand. Set CMAKE_EXPORT_COMPILE_COMMANDS=ON once in your initial configure so your IDE stops nagging you to rebuild for IntelliSense.

Fix 2: Enable ccache Properly

ccache is free speed. Zephyr supports it natively:

west build -b nrf52840dk/nrf52840 app -- \
  -DCMAKE_C_COMPILER_LAUNCHER=ccache \
  -DCMAKE_CXX_COMPILER_LAUNCHER=ccache

Default cache size is 5GB, which fills up fast on Zephyr. Bump it:

ccache -M 20G

Verify it’s actually working. Run ccache -s before a clean build, then after. On a second build of the same tree you should see >90% hit rate. If you see 0%, your compiler launcher isn’t wired up, or your toolchain path keeps changing.

Gotcha: ccache misses when -march or -mcpu flags shift, which happens silently when you switch boards. Pin your Zephyr SDK version and don’t mix toolchains across projects.

Fix 3: Tune Ninja and Your Toolchain

Ninja auto-parallelizes, but it’s worth verifying it’s using every core:

CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) west build

On a laptop, the bottleneck is rarely cores. It’s thermals and IO. Run htop and iostat during a cold build. If you see CPU pegged for 10 seconds then dropping to 40%, you’re thermally throttled. If you see disk wait, your build folder is on a slow drive. Move it to NVMe.

Linking is often single-threaded and surprisingly slow. Switch to lld or mold where supported:

-DCONFIG_LLD=y

mold can cut link time by 3-5x on larger apps. And keep your Zephyr SDK current. The 0.16.x and 0.17.x SDKs ship measurably faster GCC builds than 0.14.

Fix 4: Slim Your Sysbuild and Module Set

Sysbuild is great for production (MCUboot + app + network core image in one shot), but it doubles or triples your configure time. Most of that is wasted in the inner loop.

For day-to-day iteration, build the app directly with west build -b nrf52840dk/nrf52840 app. When you actually need to test the bootloader path, switch to sysbuild explicitly, and don’t make it the default for regular work.

Audit your west.yml. Every module gets parsed at CMake configure time, even if you don’t use a single symbol from it. If you’re not using LVGL, TF-M, or the cellular modem subsystem, drop them from the manifest. A trimmed manifest can shave 5-15 seconds off cold configure. The Zephyr device integration guide is a good reference for what you actually need versus what got pulled in by a vendor sample.

For inner-loop iteration:

CONFIG_BOOTLOADER_MCUBOOT=n

Flip it back for release.

Fix 5: Use Multiple Build Directories

One build directory per board/config combination. Switching boards inside a single build dir forces a full reconfigure and often a full rebuild.

Lay it out like this:

build/
  nrf52840dk_dev/
  nrf52840dk_release/
  nrf5340_dev/
  qemu_cortex_m3/

Then:

west build -d build/nrf52840dk_dev -b nrf52840dk/nrf52840 app
west build -d build/nrf5340_dev -b nrf5340dk/nrf5340/cpuapp app

Each stays warm, and switching between them is instant because nothing invalidates.

For tight iteration on a single file, skip west entirely and run Ninja directly:

ninja -C build/nrf52840dk_dev

This shaves a couple of seconds off each invocation by avoiding west’s Python startup.

If you’re working with Hubble’s BLE stack, the Zephyr reference application is set up this way out of the box.

A Quick Word on CI

CI is where the 5-minute build hurts most because it blocks PRs.

  • Run ccache with an S3 or GCS backend so cache survives across runners.
  • Cache the entire build/ directory keyed on a hash of west.yml + all prj.conf files + Kconfig fragments. If the hash matches, restore and run incremental. If not, cold build and update the cache.
  • On GitHub Actions, actions/cache works fine for small projects. For anything serious, use self-hosted runners with persistent ccache volumes. The difference is 8 minutes vs 90 seconds.
  • Don’t --pristine in CI either. Seed from a cached build when the manifest matches. Only pristine on release tags.

The CI conversation deserves its own post. The short version: if your local build is fast, your CI build can be fast with the same tricks plus a remote cache.

Before and After

A real project you might recognize, nRF52840 app with BLE and a sensor stack:

                  Before    After
Cold build        5m 12s    3m 40s
Incremental (1f)  4m 48s    18s
Branch switch     5m 20s    45s

The cold build improvement came from ccache (warm cache from the previous branch) and mold. The incremental fix was 90% “stop running pristine” plus killing an unused module. Branch switch dropped sharply once each branch had its own build directory and they stopped invalidating each other.

Do This Afternoon

Work the list in order:

  1. Run ninja -d stats and the CMake trace. Find out which phase is slow.
  2. Remove every --pristine from your aliases and scripts.
  3. Wire up ccache and verify hit rate with ccache -s.
  4. Switch your linker to lld or mold.
  5. Drop sysbuild and MCUboot from your dev config.
  6. Create one build directory per board.

Measure before and after each fix. If a step doesn’t move your number, undo it and move on. The goal is sub-30 seconds for a single-file change. If you’re not there after working this list, your problem is something stranger and worth a profiler trace. Send me the trace.


Hubble Network handles firmware OTA and device connectivity over BLE without requiring gateways or pairing, so your build-test loop stays focused on code, not infrastructure. See how it works →