This is one of the first architectural decisions in any firmware project, and it's easy to make by habit rather than by evaluating the product's actual requirements. A bare-metal superloop and a full RTOS (FreeRTOS, Zephyr, ThreadX, and similar) solve different problems, and picking the wrong one shows up later as either wasted engineering effort on unnecessary infrastructure, or as an unmaintainable tangle of interrupt handlers and state machines trying to fake concurrency.

What bare-metal actually means in practice

A bare-metal design runs a single main loop, typically structured as a superloop or a simple state machine, with interrupt service routines handling time-critical events and setting flags the main loop checks. There's no task scheduler, no context switching, and no dynamic memory allocation in most well-written bare-metal firmware. That simplicity is the whole point: every code path is traceable by reading the loop top to bottom, memory usage is static and predictable, and there's no scheduler overhead eating into the power or timing budget.

Bare-metal is the right default for firmware with a small, fixed set of responsibilities — read a sensor, process it, transmit or store the result, sleep — especially on memory-constrained MCUs where an RTOS's RAM footprint would eat into an already-tight budget.

What an RTOS buys you, and what it costs

An RTOS earns its place when a product has multiple genuinely independent activities that need to run with their own timing guarantees — for example, a BLE stack that must service its own timing-sensitive events, a sensor-sampling task on a fixed interval, a UI or display update loop, and a background OTA download, all coexisting without one blocking the others. Trying to interleave that by hand in a bare-metal superloop gets fragile fast, whereas an RTOS's preemptive scheduler handles it by design.

The cost is real: RAM and flash overhead for the kernel and each task's stack, added complexity from needing to reason about task priorities, shared-resource locking (mutexes, semaphores) and potential priority inversion, and a steeper debugging curve — a race condition in a multi-tasked RTOS design is categorically harder to track down than a bug in a linear superloop.

A practical framework

SignalLeans bare-metalLeans RTOS
Number of concurrent responsibilities1-2, sequential3+, genuinely independent
Timing requirementsSimple, loop-drivenMultiple independent deadlines
Available RAM/flashTight (sub-32KB RAM class)Comfortable headroom
Connectivity stackNone or simple pollingBLE/Wi-Fi stack needing its own scheduling
Team's RTOS experienceLimited — risk of misuse outweighs benefitEstablished — mutexes/priorities handled correctly

None of these signals is decisive alone — a product with a BLE radio and 128KB of RAM might still be better served by a well-structured bare-metal design with a lightweight cooperative scheduler, if its concurrency needs are modest. The decision is worth making deliberately at the architecture stage, because migrating from one model to the other mid-project is expensive.

A middle ground: cooperative schedulers

Between the two extremes sits a class of lightweight cooperative task schedulers that provide some of an RTOS's structure — named tasks, defined intervals — without preemptive context switching or the associated RAM overhead. These fit products with moderate concurrency needs that don't justify a full RTOS but have outgrown a raw superloop's readability.

How we approach this

We treat this as an architecture decision made explicitly during the design phase, not a default choice, weighing the product's actual concurrency and timing needs against its memory budget and the team's RTOS fluency. See our firmware work for how this fits into a broader engagement, from bring-up through OTA-ready delivery.