During Rescue Sprints with clients building flight controllers, motor drives or robotic nodes, one of the first questions a CTO asks me is: „We stayed with FreeRTOS, but should we have picked Zephyr instead?”. It usually comes too late - once the architecture is frozen and the team starts hitting the limits of the kernel they chose.
Zephyr vs FreeRTOS is not a matter of taste or GitHub popularity. It is a choice about your model of determinism, your memory budget, and how hard (or easy) it will later be to prove that the system behaves correctly in the worst possible case. Below I break the decision down into the factors that actually matter in a safety-critical project - with the math you need to understand why a given RTOS behaves the way it does, not just that it does.
1. The scheduler model: what a priority actually guarantees
At their core, both kernels are fixed-priority preemptive schedulers. A higher-priority task always preempts a lower one. The devil lives in what happens at equal priority and what alternatives you have.
- FreeRTOS: purely priority-based, with optional round-robin (time-slicing) for threads of equal priority when
configUSE_TIME_SLICINGis enabled. Simple, predictable, minimal. - Zephyr: priority-based with a distinction between cooperative threads (negative priorities, non-preemptible) and preemptible threads (positive priorities), plus an optional EDF (Earliest Deadline First) scheduler via
CONFIG_SCHED_DEADLINEand meta-IRQ threads. A richer model, more tools, but also more surface for mistakes.
The key point: having priorities guarantees nothing by itself. A task set can be unschedulable even under a perfect scheduler if total CPU load exceeds the theoretical bound. That is where the classic result comes in.
Under Rate Monotonic Scheduling (RMS), where the task with the shorter period gets the higher priority, Liu and Layland (1973) proved an upper bound on CPU utilization below which a set of n independent periodic tasks is always schedulable:
where Cᵢ is the execution time (WCET) of task i and Tᵢ is its period. For n = 1 the bound is 1.0 (100%), for n = 2 it drops to 0.828, and in the limit for a large number of tasks:
Which means: with many tasks, once total CPU load crosses roughly 69%, RMS may no longer guarantee that deadlines are met, even though the CPU „still has 30% idle”. Too few teams know this number - and it is exactly why an overloaded processor starts dropping frames before the watchdog notices anything.
In practice both FreeRTOS and Zephyr let you implement RMS by manually assigning priorities by period. The difference is that Zephyr additionally offers EDF, which can theoretically reach U ≤ 1.0 - at the cost of harder worst-case analysis and higher scheduler overhead.
2. Priority inversion, and why a mutex is not a semaphore
The most insidious real-time bug never shows up in unit tests. It is priority inversion: a high-priority task waits on a resource held by a low-priority task, which is in turn preempted by medium-priority tasks. The net effect is that „high” waits on „medium”, which should never happen.
The textbook example is Mars Pathfinder (1997), where priority inversion on the data bus caused repeated resets of the whole lander. More important than the anecdote is the protection mechanism: the Priority Inheritance Protocol.
- FreeRTOS: mutexes (unlike binary semaphores) implement basic priority inheritance - the task holding the mutex temporarily inherits the priority of the highest-priority blocked waiter. There is no full Priority Ceiling Protocol.
- Zephyr: mutexes likewise inherit the priority of the highest-priority waiting thread, with a similar restoration mechanism on release.
The design consequence is one hard rule: for critical sections always use a mutex, never a binary semaphore, to protect a shared resource. A semaphore has no owner, so it cannot boost anyone's priority.
Priority inheritance does not eliminate blocking - it bounds it and makes it computable. A high-priority task can be blocked for at most the longest critical section of the lower-priority tasks that share a resource with it:
This blocking term Bᵢ feeds directly into the schedulability test. The extended Liu & Layland condition for task i (accounting for all higher-priority tasks j and for blocking) becomes:
The takeaway: every critical section held by a low-priority task „steals” timing budget from the high-priority one. That is why in safety-critical firmware critical sections must be short and bounded - this is not a style preference, it is a variable in the inequality that decides whether the deadline holds.
If you cannot state an upper bound on the longest critical section in your system, you cannot compute Bᵢ - and without Bᵢ your schedulability analysis is wishful thinking, not engineering.
3. Interrupt latency and the cost of a context switch
Determinism is measured in clock cycles, not declarations. Here FreeRTOS has an edge that follows directly from its minimalism: a thinner kernel means a shorter path from interrupt to response.
- FreeRTOS on Cortex-M4: a context switch typically fits in ~1-3 µs at 168 MHz, with the core switching threads in the PendSV handler. Interrupt entry latency on Cortex-M is a hardware ~12 NVIC cycles (the core itself stacks the context).
- Zephyr: the overhead is noticeably higher, because the handling path runs through a richer kernel (device model, driver subsystem, optional tracing/logging). This is a real trade-off - you pay a few hundred nanoseconds to a couple of microseconds more for far richer infrastructure.
The real reaction time to an event is a sum of terms, each of which you can measure on a scope or in the cycle counter (DWT->CYCCNT):
Let us compute it on a Cortex-M4 clocked at 168 MHz. One cycle is:
The hardware latency to enter an interrupt vector (~12 NVIC cycles) is about 12 × 5.95 ns ≈ 71 ns. Add the ISR itself, the context switch (say 2 µs in FreeRTOS), and only then the actual task. If your drone stabilization loop runs at 1 kHz (a 1000 µs period) and total per-cycle kernel overhead is 3-4 µs, you „lose” ~0.4% of the budget to the RTOS alone. At 8 kHz (125 µs) that same overhead is already ~3%, and the choice of kernel starts to matter in a measurable way.
Do not trust whitepaper numbers. Measure the context switch on your own target, with your compiler configuration and optimization level - it is one of the first tasks I run in a firmware audit.
4. The memory budget: the price of a rich ecosystem
This is where the difference is most tangible and most often decides the choice on small ARM Cortex-M microcontrollers (ST, TI, Nordic, Silicon Labs).
- FreeRTOS: the kernel alone is typically ~6-15 KB ROM, and the per-task RAM cost is the TCB block (tens to about 100 bytes) plus a thread stack you size yourself. It fits comfortably on a Cortex-M0+ with 32 KB of flash.
- Zephyr: depending on enabled subsystems (device tree, logging, network stack, shell) it realistically ranges from ~20 to 100+ KB ROM. A minimal configuration can go quite low, but as soon as you bring in BLE, USB or TCP/IP the footprint grows fast.
This is not a flaw in Zephyr - it is the price of its strengths. You pay in memory for a ready, tested device model and drivers you would otherwise have to write (and maintain) yourself under FreeRTOS. On an MCU with 1-2 MB of flash it is irrelevant. On a sensor node with 64 KB of flash it can be a go/no-go decision.
5. WCET, static allocation and the road to certification
For systems I design with a rigor close to DO-178C DAL-C (an approach inspired by the standard, not a formal certification), one thing matters above all: whether you can compute and prove the WCET (Worst-Case Execution Time) of every significant path.
WCET is computable only when you remove sources of non-determinism. The two pillars are:
- Static memory allocation: zero
malloc/freeat runtime. FreeRTOS supports this directly throughxTaskCreateStaticand its siblings, where every kernel object gets a buffer you provide. Zephyr likewise allows fully static definition of threads and kernel objects at compile time. A dynamic heap is the enemy of WCET - fragmentation and unbounded allocation time break the analysis. - A deterministic scheduler: fixed, bounded kernel operation times, so that RTOS overhead is a constant term in the timing budget rather than a random variable.
WCET ties directly to test coverage. To demonstrate that you computed the worst case for every branch of the logic, you use MC/DC coverage (Modified Condition/Decision Coverage) - the same level of rigor DO-178C requires for DAL-A/B. On top of that comes FMEA at the architecture level, to map failure modes onto protective mechanisms.
The link to the standards is explicit: safety integrity classes (IEC 61508 SIL, ISO 26262 ASIL in automotive, DO-178C DAL in avionics) require documented timing determinism and structural coverage. Your RTOS choice affects how much of that proof work lies ahead of you.
- FreeRTOS has the commercial SafeRTOS variant (WITTENSTEIN) with pre-certification packages for IEC 61508 / ISO 26262 / IEC 62304 - if certification is the goal, that is a real path.
- Zephyr is developing functional-safety efforts (a Safety Working Group), but the richness of the kernel means a larger code surface to qualify. A smaller codebase is sometimes easier to defend in an assurance argument.
6. Memory protection: MPU, watermarking and isolation
The last dimension is fault isolation - critical when a single stray pointer must not be able to bring down the whole system.
- Stack overflow detection: both kernels support stack watermarking (filling the stack with a pattern and checking how deep it went), which lets you size stacks with margin instead of guessing. In FreeRTOS this is
uxTaskGetStackHighWaterMark. - MPU protection: here Zephyr is ahead - it has a mature userspace built on the hardware MPU, isolating threads and enforcing memory boundaries between them. FreeRTOS has a FreeRTOS-MPU variant, but historically more limited and less commonly deployed.
If your architecture requires separation of safety domains (for example, isolating critical from non-critical tasks on a single MCU), Zephyr's hardware userspace is a genuine argument that open-source FreeRTOS does not match.
Summary: when to pick which
Let us reduce this to concrete decision criteria rather than an academic tie.
Choose FreeRTOS when:
- you are on a small MCU (tens of KB of flash) and every kilobyte counts;
- you need the thinnest, most predictable kernel with the lowest context-switch overhead;
- you want a minimal codebase to review/qualify, with SafeRTOS as your certification path;
- your system is a compact set of control tasks without heavy connectivity.
Choose Zephyr when:
- you need a rich ecosystem out of the box: BLE, USB, TCP/IP, LoRa, a coherent device model and device tree;
- you require hardware memory isolation (userspace/MPU) between safety domains;
- you have ROM/RAM headroom (hundreds of KB to MB of flash) and want portability across MCU families;
- you are considering advanced scheduling strategies (EDF) or multicore.
The worst scenario is a choice made by reflex - „because that is what we used last project” - followed by months of fighting the consequences. If your team faces this decision, or is already stuck with an RTOS that does not close its timing or memory budget, get in touch. In a Rescue Sprint I run the schedulability analysis, measure the real WCET and context switch on your target, and deliver a recommendation (or a fix) on your own codebase within 1-2 weeks - with numbers, not opinions.
