Zephyr vs NCS: Understanding the Relationship Between Upstream Zephyr and Nordic's Fork

Diagram comparing upstream Zephyr RTOS and Nordic Semiconductor's NCS fork side by side

You grab a Zephyr sample from the official docs, drop it into your nRF Connect SDK project, hit build, and watch it fail. The API exists in the Zephyr docs you just read. It should be there. You triple-check the function signature. It’s correct. But the compiler disagrees.

You’re not losing your mind. You’re working in two codebases that look almost identical but aren’t.

This confusion hits almost every developer who works with Nordic hardware and Zephyr. NCS and upstream Zephyr share a bloodline, but they’re different animals with different release schedules, different defaults, and different behavior in places you wouldn’t expect. Let’s strip this relationship down to something you can actually reason about.

Fork, Pin, Patch: How NCS Actually Relates to Zephyr

NCS (the nRF Connect SDK) is a product SDK built by Nordic Semiconductor. It contains Zephyr, but it isn’t Zephyr. Here’s the architecture:

┌─────────────────────────────────┐
│         Your Application        │
├─────────────────────────────────┤
│  sdk-nrf  │  sdk-nrfxlib  │ ... │  ← Nordic-only modules
├───────────┴───────────────┴─────┤
│   sdk-zephyr (Nordic's fork)    │  ← Zephyr + Nordic patches
│   pinned at commit abc1234...   │
├─────────────────────────────────┤
│      Upstream Zephyr Project    │  ← What zephyrproject.org ships
└─────────────────────────────────┘

Three things to notice here.

First, NCS uses a west manifest that pins a specific Zephyr commit. Not a release tag. A commit hash. Your NCS workspace contains a frozen snapshot of Zephyr, not the latest anything.

Second, that snapshot lives in Nordic’s fork repo (nrfconnect/sdk-zephyr), where Nordic applies their own patches on top. BLE fixes, driver errata workarounds, performance tweaks for nRF chips. These patches exist only in Nordic’s fork.

Third, NCS pulls in modules that sit alongside Zephyr but aren’t part of it. sdk-nrf contains Nordic-specific libraries, applications, and board support. sdk-nrfxlib contains closed-source and proprietary components like the SoftDevice Controller. These have no upstream equivalent.

When Nordic says “NCS v2.9.0 is based on Zephyr v4.0,” the phrase “based on” is doing a lot of heavy lifting. It means they rebased their fork onto a Zephyr release, applied their patch set, and validated the result. What you get is Zephyr-ish. Close enough to be familiar. Different enough to break your assumptions.

The Five Friction Points Between NCS and Zephyr

Knowing where the two codebases diverge is what saves you time.

Zephyr Version Lag

NCS releases typically trail upstream Zephyr by weeks to months. A feature that landed in Zephyr main last month probably won’t show up in NCS until the next release cycle, sometimes two. If you’re reading a blog post about a shiny new Zephyr API and you’re on NCS, check the version first. You might be one or two Zephyr releases behind.

You can verify this fast. Run west list, find the zephyr entry, and compare the pinned revision against upstream release tags. That tells you exactly how far behind you are.

Patched Drivers and HAL Differences

Nordic patches BLE, SPI, UART, I2C, and other drivers for nRF-specific silicon errata and performance tuning. The nrfx HAL layer, which wraps Nordic’s hardware registers, gets special treatment in sdk-zephyr that doesn’t exist upstream.

These patches are usually invisible. Until they’re not. A driver that behaves one way upstream may have subtly different timing, error handling, or power characteristics in NCS. If you’re debugging a peripheral issue and the upstream Zephyr source doesn’t match what you’re seeing, this is probably why.

Kconfig Default Overrides

This one is sneaky. NCS sets different Kconfig defaults than upstream Zephyr for things like BLE stack configuration, logging backends, memory allocators, and thread stack sizes. A bare prj.conf that compiles and runs fine on upstream Zephyr may produce completely different runtime behavior under NCS, because the defaults you didn’t explicitly set are different.

You won’t see this in your config file. You’ll see it in the merged output. Run west build -t menuconfig and actually look at what’s been selected. Compare that against what upstream Zephyr would select with the same prj.conf. The deltas can be surprising.

Devicetree and Board Definitions

Nordic maintains its own board definitions in sdk-zephyr that may diverge from their upstream equivalents. Pin mappings, peripheral assignments, and clock configurations can differ between the Nordic-maintained board DTS and the upstream version.

Zephyr 3.6 introduced a new hardware model, adopted in NCS 2.6+. This made the transition period particularly rough. If you’re on an NCS version that straddles this migration, you may find board definitions in a half-migrated state that doesn’t match any upstream documentation cleanly.

API Deprecation Timing

Upstream Zephyr deprecates APIs aggressively. Functions get marked deprecated, sit for a release or two, then get yanked. NCS sometimes retains deprecated APIs longer because Nordic prioritizes stability for production customers. Other times, Nordic removes things early for reasons specific to their fork, like replacing an API with a Nordic-optimized alternative.

Either way, the deprecation timeline doesn’t match. Code that compiles with warnings on upstream Zephyr might compile cleanly on NCS (deprecated API still present) or fail entirely (Nordic removed it early). You can’t assume the warning-to-removal timeline is the same.

Here’s a summary view:

Friction Point        │ Upstream Zephyr        │ NCS (sdk-zephyr)
──────────────────────┼────────────────────────┼──────────────────────
Zephyr version        │ Latest main/release    │ Pinned, weeks-months behind
Drivers               │ Generic                │ Nordic-patched (errata, perf)
Kconfig defaults      │ Zephyr defaults        │ Nordic-overridden defaults
Board definitions     │ Upstream boards/       │ Nordic-maintained variants
API deprecation       │ Aggressive timeline    │ Different timeline

How to Inspect the Divergence Yourself

You don’t have to take anyone’s word for how far apart the two codebases are.

Start with west list in your NCS workspace. This shows every module, its repository URL, and its pinned revision. Find the zephyr entry. That commit hash is your ground truth.

Then look at the west.yml manifest in sdk-nrf. It’s the bill of materials for the entire NCS workspace:

sdk-nrf/west.yml
  ├── zephyr (pinned: sdk-zephyr @ <commit>)
  ├── nrfxlib
  ├── mcuboot
  ├── trusted-firmware-m
  └── ... other modules

For a direct code comparison, diff Nordic’s fork against upstream on GitHub. Go to nrfconnect/sdk-zephyr and compare the relevant branch against the corresponding zephyrproject-rtos/zephyr tag. GitHub’s compare view will show you every Nordic-specific patch.

Check the NCS release notes too. Nordic publishes a “Zephyr fork” section that lists their changes. It’s not always exhaustive, but it’s a starting point.

Practical Strategies for Working Across Both

Pick your base and stay on it

If you’re shipping a product on Nordic hardware, develop against NCS. Don’t copy-paste from upstream Zephyr docs or samples without first checking that the APIs and configs exist in your NCS version. Treat upstream Zephyr documentation as a reference, not gospel.

Isolate your portable code

Keep business logic and hardware-abstraction layers in standalone libraries. Depend only on Zephyr APIs (not sdk-nrf or nrfxlib APIs) for anything you want to move between ecosystems. If you’re building a project that might need to run on non-Nordic hardware someday, this discipline pays for itself fast. The Hubble Device SDK is a good example of firmware designed to work across different platform integrations while keeping a clean abstraction boundary.

Track the version mapping

Keep a simple reference of which NCS version maps to which Zephyr version:

NCS Version  │  Zephyr Base   │  Notable Gaps
─────────────┼────────────────┼──────────────────────
v2.9.0       │  Zephyr v4.0   │  (check release notes)
v2.8.0       │  Zephyr v3.7   │  (check release notes)
v2.7.0       │  Zephyr v3.6   │  HW model transition
v2.6.0       │  Zephyr v3.5   │  

(Verify against current NCS release notes; these shift with each release.)

Contribute upstream first

If you write a driver or feature you want in both ecosystems, submit it to upstream Zephyr first. Once it’s merged upstream, it’ll flow into NCS when Nordic rebases. Going the other direction (NCS to upstream) is harder because Nordic-specific patches may not be accepted upstream.

Use CI to catch divergence

If portability matters, maintain CI jobs that build against both upstream Zephyr and NCS. This catches API breakage, Kconfig differences, and missing dependencies early. Two build targets, one codebase. The cost is a few extra CI minutes. The alternative is finding out during a port that takes days.

If you’re evaluating how to set up device firmware that works with a specific Zephyr-based platform, the Hubble Zephyr reference application shows one approach to structuring a clean Zephyr project.

When Upstream Zephyr Makes More Sense Than NCS

There are legitimate reasons to use vanilla Zephyr even on Nordic chips.

Multi-vendor projects where you’re targeting Nordic, STM32, and ESP32 from one codebase benefit from staying on upstream Zephyr. The moment you depend on sdk-nrf, you’ve locked yourself to Nordic.

If you need a bleeding-edge Zephyr feature that hasn’t made it into NCS yet, upstream is your only option (short of cherry-picking, which gets messy).

Simple projects that don’t need BLE, or don’t need Nordic’s optimized SoftDevice Controller, may find NCS’s extra modules and complexity unnecessary. A basic sensor node doing SPI reads and UART output doesn’t need nrfxlib.

The tradeoff is real though. You lose Nordic’s optimized BLE stack, their support pipeline, and their validated board configurations. For production BLE products on Nordic silicon, that’s usually too much to give up.

Read Your Own Manifest

NCS contains Zephyr but isn’t Zephyr. The divergences are predictable once you know where to look: version lag, patched drivers, Kconfig overrides, devicetree differences, and mismatched deprecation timelines.

Open a terminal. Run west list in your NCS workspace. Look at the Zephyr commit hash. Compare it against upstream. Read the diff. That ten-minute exercise will teach you more about your build environment than any documentation page. Once you see the gap clearly, you can stop fighting it and start planning around it.


Hubble Network enables direct-to-satellite connectivity for low-power IoT devices without the complexity of traditional network infrastructure. See how it works →