A user interface on a microcontroller has one unforgiving rule: it must keep painting. There is no compositor thread to hide a stalled handler, no browser process to kill, and no spare core dedicated to keeping the frame loop alive. If app code spins, the device appears to freeze.

Micro OS does not pretend otherwise. It bounds every handler.

One event at a time

The runtime has a FIFO event queue. Pointer input becomes an activation handler ID, the host appends it, and tick() processes exactly one event. The VM executes the handler with a default budget of 10,000 instructions.

The number is not a performance target. It is a failure boundary. A handler that does useful work in a few hundred instructions has plenty of headroom; a handler that loops forever hits a wall while the host can still recover.

EventQueue → VM handler → StateStore → bindings → RenderPatch
                 │
                 └─ budget: 10,000 instructions

When the budget is exhausted, the handler stops. Later events remain processable. This is the important part: a runaway handler does not poison the queue.

State before exhaustion is kept

Stopping mid-handler could leave the UI and the state disagreeing. The runtime avoids that by committing what already happened. State writes completed before exhaustion remain committed, and every dirty binding is flushed, so the screen reflects the state the app actually reached.

The rule is conservative: a partial handler is still a consistent prefix of its effects. The app does not get to observe a half-written state slot, and the renderer does not get to observe a half-flushed binding set.

Failure is local

The host policy differs because the platforms differ.

Host On app failure
ESP32-S3 Return to Home; the shell keeps running
macOS Report the failure and keep the process alive
Web Stop the frame loop and surface the error

The shell is the exception. Home is itself an MBC app, so an unreadable shell.mbc aborts boot instead of falling back to native UI. That is a known limitation, not a design goal; a native fallback is on the target list.

What the budget is not

It is not a scheduler. There are no background apps, no timers, no preemption and no IPC. The host runs one foreground app and drives the runtime from its own UI thread or task. The budget bounds one handler; it does not create concurrency.

It is also not a security boundary in the memory-protection sense. Apps execute in one address space as bytecode, sandboxed by the VM and the host API, not by an MMU. The capability gate is default-deny at the call boundary, but the manifest is unauthenticated and there is no hardware isolation on the ESP32-S3.

What the budget does buy is predictability: a slow or broken app cannot silently consume the frame loop forever. On a device where the only recovery gesture is a physical button, that is worth more than raw throughput.

Choosing the number

10,000 is empirical. It is large enough for the shipped apps — momo’s review handlers, Settings’ Wi-Fi state transitions, Counter’s arithmetic — and small enough that exhaustion is measured in milliseconds, not seconds. The VM tests include an exact-budget case: a handler that uses every instruction succeeds, and one more fails.

The limit lives in the runtime, not in app code. Apps cannot opt out, and there is no “long task” escape hatch. If a future app needs more work per event, the right move is to split the work across events, not to raise the ceiling quietly.

That is the whole design: a small, explicit promise about how much can happen before the host gets control back.