We regularly see embedded projects reach for an RTOS by default, treating it as the more 'serious' engineering choice compared to bare-metal firmware. That instinct is backwards. An RTOS solves specific problems — and if your product doesn't have those problems, it adds complexity, memory overhead and a new class of bugs (priority inversion, stack sizing errors, race conditions) without buying you anything.
Bare-metal is the right choice when your application has a small, well-defined number of tasks with simple timing relationships — a superloop with interrupt-driven I/O can be more deterministic and easier to reason about than a poorly architected RTOS application. It's also the right choice for extremely resource-constrained MCUs where RTOS overhead competes meaningfully with your available RAM and flash.
An RTOS earns its place when you have genuinely concurrent, independently-timed responsibilities: a communication stack that needs to run on its own schedule, a control loop with a hard real-time deadline, and a user-interface task that shouldn't block either — with priorities that actually need to be enforced by a scheduler rather than manually interleaved in a loop. It also helps significantly once a firmware codebase grows large enough that isolating subsystems into separate tasks makes the code more maintainable, not just more concurrent.
The actual decision framework we use: count your genuinely independent timing domains. One or two, with simple interrupt-driven I/O — stay bare-metal. Three or more, with real priority conflicts between them, or a team large enough that module isolation matters more than raw simplicity — that's when an RTOS like FreeRTOS or Zephyr starts paying for its added complexity instead of just adding it.