·9 min read

A/B Updates Done Right: Why Your Field Devices Need Two Root Filesystems

OTA UpdatesA/B UpdatesEmbedded LinuxYoctoSWUpdateRAUC

Every industrial device that ships with a single root filesystem is one bad update away from a field visit. The failure mode is depressingly common: an update writes over the running system, the power drops or a file corrupts mid-write, and the device never boots again. There's no software left to fix it, because the broken thing is the software. Someone drives out with a cable and a reflash tool.

A/B updates exist to make this failure mode structurally impossible. The idea is simple — keep two complete copies of the system, update the one you're not running, verify it, and only then switch over. The details, as always, are where teams get it wrong. This is the practical version.

The core idea: never update what you're standing on

In an A/B layout, the storage has (at minimum) two root filesystem partitions: A and B. One is active — the system is booted from it right now. The other is inactive and sits idle. When an update arrives, it is written only to the inactive partition. The running system is untouched throughout the entire process, so a power cut, a network drop, or a corrupted payload can never leave the device without a bootable system.

Once the update is fully written, it gets verified — checksums or signatures, ideally both — and only then does the bootloader's "which partition to boot" flag flip. On the next reboot, the device boots the new partition. The old one remains intact as an instant fallback.

This is what "atomic" means in practice. The update either completes entirely and gets marked good, or the device keeps running the previous system as if nothing happened. There is no half-updated state, because the half-updated partition is never the one you're booted from.

What the partition layout actually looks like

A typical A/B layout on an industrial device (eMMC or SSD):

  • Bootloader (U-Boot or similar) — knows which rootfs slot is active, and how to fall back.
  • rootfs A and rootfs B — two complete, independent system images. Each must be self-contained: kernel, device tree, libraries, your application, everything.
  • Data partition — shared between both slots. Configuration, logs, application databases, credentials. This partition is not duplicated, which is a deliberate design decision with real consequences (more below).
  • Boot-state storage — a small, durable record of which slot is active, how many boot attempts have failed, and whether the running slot has been confirmed healthy. This lives in U-Boot environment storage, a dedicated flash region, or sometimes the bootloader's own redundant environment.

The bootloader's job during an update cycle: boot the slot marked active. If that slot fails to reach a healthy state (the OS never signals "I'm up and running"), the bootloader retries — and after a configurable number of failed attempts, flips back to the other slot automatically.

Rollback logic: the part most teams under-design

The atomic switch is only half the story. The harder half is deciding when to give up on the new slot and go back. There are two failure modes to handle:

1. The new system doesn't boot at all. The bootloader attempts to boot slot B, the kernel panics or the rootfs is unreadable, and the device never reaches userspace. The fix is a boot counter: the bootloader increments a counter each attempt, and if the count exceeds a threshold (typically 3), it flips back to slot A permanently and marks B as bad. U-Boot's bootcount mechanism with a redundant environment is the standard implementation on Yocto-based systems. The critical detail: the boot counter storage itself must survive the crash, so it lives in NOR flash or a protected eMMC region — never in the rootfs you're trying to boot.

2. The new system boots but the application is broken. This is the subtler failure. The kernel comes up, userspace starts, and your application crashes on launch — or starts but misbehaves (can't reach the network, sensor reads are garbage). A boot counter won't catch this, because from the bootloader's perspective, the boot "succeeded."

Handling this requires health confirmation from the running system: after boot, a service verifies the application is actually functional — a watchdog, a self-test routine, a connectivity check — and only then marks the slot as confirmed good. If confirmation doesn't arrive within a timeout, the system reboots, the bootloader's attempt counter increments, and after enough failures the device rolls back to the known-good slot. Android's update_engine pioneered this pattern; on industrial Yocto devices it's typically implemented with a systemd service that flips a flag once the application reports healthy.

The failure teams actually ship is subtler still: they implement the boot counter but skip the application-health confirmation. The update boots, the app is broken, and the device sits there — running, unreachable, useless — in the new slot forever. Test the broken-app path explicitly, not just the unbootable-image path.

The shared data partition problem

Because both slots share one data partition, your update has to be compatible with the data that's already there. A new application version that expects a migrated database schema will break if the migration runs against old data — and a rollback after a partial migration can leave the data in a state neither version understands.

The practical rules:

  • Make data migrations forward-only and idempotent. Running the migration twice must be safe, and an old version reading new-schema data should degrade gracefully or refuse with a clear error — not corrupt it.
  • Version your data schema explicitly. The application checks a schema version on the data partition at startup and knows how to upgrade from any prior version you actually shipped, not just the immediately previous one.
  • Never store slot-specific state in the shared partition unless the key includes the slot or version. Temp files, caches, and lock files that assume the current app version will confuse a rolled-back system.

This is the most common source of "the rollback worked but the device is still broken" incidents. Design the data story before the partition story.

Update agents: SWUpdate, RAUC, and Mender

You don't have to build the download-verify-write-flip machinery yourself. Three options cover most industrial Yocto deployments:

SWUpdate is the workhorse. It's a daemon that runs on the device, pulls update images (single signed .swu files containing everything — rootfs, kernel, bootloader bits), verifies signatures, writes to the inactive slot, and talks to U-Boot to flip the boot flag. It's mature, well-suited to custom hardware, and integrates cleanly with Yocto through meta-swupdate. The tradeoff: you bring your own update server and fleet management. If you have ten device types and a custom backend, this is usually the right call.

RAUC takes a similar approach with a stronger focus on the bootloader handshake and slot semantics. Its bundle format is flexible, it handles the U-Boot/GRUB/EFI boot-slot switching with explicit state tracking, and the Yocto integration (meta-rauc) is solid. Teams that care most about the correctness of the slot state machine tend to land here. Like SWUpdate, fleet orchestration is your problem.

Mender is the full-stack option: client, server, and a hosted or self-hosted management backend with phased rollouts, device grouping, and deployment dashboards. The client handles the A/B mechanics; the server handles the fleet. The tradeoff is coupling — you're buying into Mender's ecosystem and its per-device pricing model rather than assembling your own. For teams that need fleet management yesterday and don't want to build a backend, it's the fastest path to production.

There's no universally right answer. The decision usually comes down to: do you already have (or want) your own update backend? If yes, SWUpdate or RAUC. If no, and you want dashboards and staged rollouts without building them, Mender.

Sizing and the honest costs

A/B updates cost storage — you're carrying two full system images. On a 4GB eMMC with a 1.2GB rootfs, the math gets tight fast, and teams sometimes try to shrink this by sharing more between slots or compressing aggressively. Don't cheat the layout: an A/B system where both slots can't independently boot isn't A/B, it's decoration.

They also cost engineering time in the places people don't budget for: the data migration story, the health-confirmation service, the manufacturing flow (both slots need a valid image at the factory, or a defined first-boot provisioning step), and the test matrix — every release now needs testing of the update path, the rollback path, and the data-compatibility path, not just the image itself.

What they buy you is the thing industrial products actually need: updates you can ship without a field-visit contingency plan. Over a ten-year product life, that's not a nice-to-have.

The practical takeaway

If your devices live somewhere you can't easily reach — a factory floor, a substation, a customer's site three states away — single-partition updates are a risk you're carrying whether you've priced it or not. A/B partitioning, a real boot-attempt counter, application health confirmation, and a data-migration story you designed on purpose (not discovered in production) turn updates from the scariest part of the product into a routine operation.

If you're weighing this for your product — the partition layout, the agent choice, the Yocto integration — we're happy to talk through it.

GatewayBoards builds custom Yocto Linux images with secure boot, A/B updates, and AWS IoT onboarding for industrial devices — A/B updates ship as part of our embedded Linux platform. Book a free scoping call.

Shipping an industrial Linux device?

We build the custom Yocto OS, AWS IoT onboarding, and update path beneath it — on your board or one we source. Start with a one-week audit and a fixed-price quote.