Skip to content
Garrett Makes It
← All writing

When to reach for an RTOS (and when bare-metal wins)

An RTOS is not a maturity badge. It's a tool with a specific job. Here's the decision I actually run through before adding a scheduler to a firmware project.

Published
Reading time
3 min read
Topics
Firmware · RTOS · Embedded Systems

There's a reflex on a lot of embedded teams to start every new project with an RTOS, the same way web teams start every project with a framework. Sometimes it's right. Often it's a scheduler, a heap, and a pile of synchronization bugs solving a problem the firmware didn't have.

Here's the decision I actually run through.

What an RTOS buys you

A real-time operating system gives you preemptive, prioritized concurrency: independent tasks that can block on events without the others stalling, and a guarantee that the highest-priority ready task runs now. That's genuinely valuable when you have multiple activities with different timing requirements that don't compose cleanly into a single loop.

The classic fit: a device juggling a hard-real-time control loop, a chatty network stack, and a sluggish user interface. Those three have wildly different deadlines. Forcing them to cooperate in one superloop means the slow thing eventually starves the fast thing, and you spend your life hand-tuning where to sprinkle poll() calls. Give each its own task with the right priority and the problem mostly dissolves.

What it costs

None of that is free:

  • RAM. Every task needs its own stack, and you will over-provision them because stack overflow is a miserable bug to chase.
  • A whole class of new bugs. Priority inversion, deadlocks, races on shared state, missed wakeups. These are concurrency bugs, and concurrency bugs are the hardest kind to reproduce.
  • Determinism you have to reason about. "Highest priority runs now" is a promise about ordering, not about your timing budget. You still have to do the analysis.

A superloop, by contrast, has exactly one execution order and you can read it top to bottom. For a lot of devices that legibility is worth more than preemption.

The heuristic

I reach for an RTOS when two or more activities have independent timing requirements that genuinely conflict — where no static ordering of a single loop satisfies all of their deadlines. If I can write the superloop and convince myself every task makes its deadline in the worst case, I don't add the scheduler.

A few tells that you've outgrown bare-metal:

  • You're nesting state machines inside the loop just to interleave two activities.
  • A blocking call somewhere (a flash write, a network round-trip) is jittering an unrelated deadline.
  • You're manually time-slicing a long operation into chunks so it doesn't hog the loop.

That last one is the giveaway. When you start hand-writing a cooperative scheduler inside your superloop, you've decided you want an RTOS — you're just building a worse one by accident.

The honest middle

There's a lot of room between "naked superloop" and "full preemptive RTOS." A timer-driven cooperative scheduler, a simple event queue, or an interrupt-plus-main-loop split handles a surprising number of cases with a fraction of the failure surface. Bare-metal with a couple of well-designed interrupts is still the right answer for most single-purpose devices I build.

The RTOS isn't the grown-up choice. It's the concurrency choice. Pick it when you have a concurrency problem — and not as a default, because the bugs it introduces are exactly the ones you least want to debug in the field.