The short answer
Go bare metal when the product does essentially one thing on a fixed cadence, memory is tight, or you need to prove exactly what the processor is doing at every instant. Add a real-time operating system when the product juggles several activities on different timescales - a wireless link, timed sensor sampling, a user interface, local storage - or when the software stacks you need are shipped as tasks that assume a scheduler exists.
The decision sounds like an internal engineering detail. It is not. It shows up in development schedule, RAM and flash requirements, the ability to add features later, and even in which microcontroller you can afford.
What actually differs
In a bare-metal architecture the firmware is one program running a main loop, interrupted by hardware interrupts when something urgent happens. There is no scheduler and no concurrency beyond interrupt context. You control every cycle, and the whole control flow can be read top to bottom.
With an RTOS you add a small management layer that splits the code into independent tasks, each with its own stack and priority, and a scheduler decides which one runs. You also get synchronization primitives - queues, semaphores, mutexes, software timers - so tasks can hand work to each other safely.
One clarification that saves a lot of confused conversations: a real-time operating system is not faster. It is more predictable. Its guarantee is that a high-priority task will get the processor within a bounded, knowable time. If you have never worked at this layer, what firmware actually is is the right place to start.
When bare metal is the right call
The simpler and more deterministic the product, the better the direct approach looks:
- A single control loop at a fixed rate. Read sensor, compute, drive output, repeat. A scheduler adds nothing here.
- Severely constrained parts. On a microcontroller with tens of kilobytes of RAM, every task stack you allocate is a meaningful fraction of the budget.
- Ultra-low average current. A device that sleeps almost all the time and wakes on one interrupt gets no benefit from a scheduling layer and pays for its tick. The techniques involved are covered in low-power firmware and sleep modes.
- Safety or certification requirements where you must demonstrate exactly what executes when. Simple code is easier to analyze, review, and argue for.
The price is that every new feature gets squeezed into the same loop. As the loop grows, guaranteeing timing gets harder, and eventually you have built a hand-rolled scheduler with none of the tooling.
When to add an RTOS
Once the product does several things at once, the answer usually tips. A device that maintains a wireless connection, samples sensors on a fixed interval, drives a display, and writes to local flash has four completely different cadences. You can weave them into one loop, but the result is a sprawling state machine that only its author understands and that breaks whenever a timing assumption changes.
The second driver is third-party software. Connectivity stacks, provisioning libraries, and update agents are frequently delivered as tasks that expect an operating system underneath. This is especially true when you need a background OTA firmware update mechanism that downloads and verifies an image while the product keeps working, or when you adopt a protocol stack with its own timing requirements. If you are choosing between specific systems, FreeRTOS vs Zephyr compares two of the common options.
And if the feature list keeps growing - a filesystem, a graphical UI, networking, multiple radios - the honest question may not be which RTOS but whether you have outgrown microcontrollers entirely. That threshold is discussed in microcontroller vs embedded Linux.
Three non-code factors that usually decide it
The part. An RTOS needs additional RAM for task stacks and kernel structures, which ties the architecture decision directly to how you choose a microcontroller. Stepping up to a slightly larger part is often cheaper than months of squeezing code, especially at moderate volumes where the unit-cost delta is a fraction of a dollar.
The team. Multitasking introduces bug classes that do not exist in a superloop: race conditions, priority inversion, deadlocks, and stack overflows that manifest as unrelated corruption. These are hard to reproduce and require experience to diagnose. A team that has not worked this way before will pay a learning cost, and it is better paid deliberately than discovered during integration.
The roadmap. If it is already clear that significant features are coming, paying the architectural cost up front beats rewriting later. If the product is genuinely finished at launch, that argument disappears.
Both paths and their cost implications are broken down further in firmware development cost.
The middle path we usually recommend
Even if you choose bare metal, write it so migration stays cheap:
- Separate the driver layer from application logic behind clean interfaces.
- Route events through a central queue rather than calling application code directly from interrupt handlers.
- Keep interrupt handlers short, and never block for long stretches in the main loop.
- Make timing explicit - a scheduled tick with named periodic jobs, not code that happens to run in the right order.
Code structured that way ports to a scheduler at reasonable cost, which means the RTOS-versus-bare-metal question does not have to be answered irrevocably on day one. A broader view of the discipline is on our embedded software hub.
Get the architecture decided before the schematic freezes
Projects House builds firmware for connected products from requirements through production, including part selection and architecture definition - and we make that call based on your actual timing, power, and roadmap requirements rather than habit. Send us your product requirements through the contact form and we will review them in depth.