How to Design a Reliable OTA Update System for BLE Devices

One bad firmware update pushed to 10,000 devices in the field. No USB port. No recovery button. No screen. Just a plastic enclosure, a BLE radio, and firmware that crashes on boot in an infinite loop. You now own 10,000 paperweights and a very expensive recall.
This scenario is entirely preventable, but only if you treat OTA as what it actually is: a system architecture, not an API call. Most developers bolt on OTA update capability late in development, wire up esp_ota_begin(), push an image over BLE, and call it done. Then they discover the hard way that a firmware over the air update system has more failure modes than the product itself.
This guide walks through the device-side architecture for a reliable BLE OTA update system: partition layout, bootloader behavior, transfer protocol, validation, and rollback. I’ll use ESP32 with ESP-IDF as the reference platform, but every decision maps to portable principles that apply on nRF52, STM32, or any MCU with sufficient flash. The focus is BLE specifically, which adds real transport constraints compared to Wi-Fi OTA.
By the end, you’ll have a coherent mental model connecting every layer and the confidence to deploy updates to devices you can’t physically touch.
Why Dual-Partition (A/B) OTA Is Your Baseline
The single most important rule of production OTA: never overwrite the running firmware. If you write a new image on top of the currently executing code and something goes wrong (power loss, corrupt download, bug in the new image), the device has nothing to fall back to.
Dual-partition OTA solves this. You maintain two firmware slots. The device always boots from one (the active slot) while writing the new image to the other (the inactive slot). If the new image is bad, the old one is still sitting there, untouched.
┌─────────────────────────────────────────┐
│ FLASH MEMORY │
├─────────────┬───────────────────────────┤
│ Bootloader │ 0x1000 - 0x7FFF │
├─────────────┼───────────────────────────┤
│ Partition │ 0x8000 - 0x8FFF │
│ Table │ │
├─────────────┼───────────────────────────┤
│ otadata │ 0x9000 - 0xAFFF │
│ (boot ctrl) │ Tracks active slot + │
│ │ validation state │
├─────────────┼───────────────────────────┤
│ ota_0 │ 0x10000 - 0x1FFFFF │ ◄── Slot A (active)
│ (Slot A) │ ~1.9 MB │
├─────────────┼───────────────────────────┤
│ ota_1 │ 0x200000 - 0x3FFFFF │ ◄── Slot B (inactive,
│ (Slot B) │ ~1.9 MB │ write target)
├─────────────┼───────────────────────────┤
│ nvs │ Storage for app config │
├─────────────┼───────────────────────────┤
│ (other) │ coredump, spiffs, etc. │
└─────────────┴───────────────────────────┘On ESP32, the partition table defines ota_0, ota_1, and a critical small partition called otadata. This otadata partition tracks which slot is active, the boot count, and the validation state of each image. It’s the bookkeeping that makes everything else work.
The sizing decision matters: each OTA partition must hold your maximum expected firmware size. On a 4MB flash ESP32, two ~1.9MB slots are typical. If your firmware grows beyond that, you’re stuck. Plan for headroom.
This principle is portable. Nordic’s nRF52 DFU supports dual-bank mode. STM32 parts with dual-bank flash do the same thing at the hardware level. MCUboot on Zephyr provides A/B slots with swap or overwrite strategies. The names change; the architecture doesn’t.
Bootloader Behavior: The Trust Anchor That Decides What Runs
Your bootloader is the single most important piece of code on the device. It runs before your application, and it decides what boots. Get this wrong, and none of the other layers matter.
Here’s the ESP-IDF bootloader flow: on reset, it reads otadata, determines which slot to boot, validates the image header, and jumps to the selected image. The critical nuance is what happens with a new image.
A freshly written image doesn’t boot as “confirmed.” It boots in a pending verification state, essentially probationary. The new firmware must explicitly call esp_ota_mark_valid_version() to confirm it’s healthy. If the device reboots before that call happens (crash, watchdog timeout, power cycle), the bootloader sees an unvalidated image and reverts to the previous slot.
Enable this with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE in your sdkconfig. If you need anti-rollback protection (preventing downgrade to older, vulnerable firmware), configure CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK with eFuse-based version counters.
The portable state machine looks the same everywhere: NEW → TESTING → VALID or NEW → TESTING → REVERT. MCUboot calls it “confirm/revert.” Nordic’s bootloader has the same concept. If you’re writing a custom bootloader, implement this state machine. It’s the core safety mechanism for any IoT firmware update strategy.
Designing the BLE Transfer Protocol for Constrained Links
Here’s where BLE OTA diverges sharply from Wi-Fi OTA. With Wi-Fi, you can HTTP GET a firmware binary in seconds. With BLE, you’re pushing a 512KB image through a pipe that negotiates MTU between 20 and 247 bytes, over a connection that a phone OS might throttle or drop at any time.
You need a chunked transfer protocol with these properties:
Chunk-based transfer with sequence tracking. Divide the firmware image into fixed-size chunks (512 bytes is a practical default). Each chunk carries a sequence number. The device writes each chunk to flash as it arrives.
Acknowledgment scheme. Pure write-with-response is safe but slow. A faster approach: use write-without-response for data chunks, with the device sending a periodic ACK notification every N chunks (e.g., every 8 or 16). If the device detects a missing sequence number, it NAKs with the expected sequence, and the phone resends from that point.
Resume capability. Persist the last successfully written flash offset in NVS. If the BLE connection drops (and it will), the phone reconnects, queries the device’s progress, and resumes from the last confirmed chunk. Without this, users will abandon the update after their third failed attempt.
GATT service design. Create a dedicated OTA service with three characteristics:
- Control (write): start transfer (sends total size, SHA-256 hash, version), abort, query status
- Data (write-without-response): chunk payload with sequence number
- Progress (notify): ACKs, error codes, validation results
Phone (Central) Device (Peripheral)
│ │
│──── OTA Start (size, hash) ───────►│
│◄─── ACK (ready, resume_offset) ────│
│ │
│──── Chunk 0 [seq=0, 512B] ────────►│
│──── Chunk 1 [seq=1, 512B] ────────►│
│──── Chunk 2 [seq=2, 512B] ────────►│
│◄─── ACK (seq=2 confirmed) ─────────│
│ │
│ ... more chunks ... │
│ │
│──── Chunk N [seq=N, final] ────────►│
│◄─── ACK (transfer complete) ───────│
│ │
│◄─── Notify (validation result) ────│Throughput reality check: With BLE 5 Data Length Extension and a 247-byte MTU, you can push a 512KB image in roughly 2–4 minutes. Without DLE on BLE 4.2, expect 15–20+ minutes. This affects your UX design and your users’ patience. Negotiate the highest MTU your stack supports.
This protocol layer sits above the BLE stack (NimBLE, SoftDevice, BlueZ) and below the OTA write logic. The pattern is identical regardless of your BLE implementation.
Three Layers of Validation That Prevent Bad Firmware From Running
A successful transfer doesn’t mean the firmware is safe to run. You need three distinct validation gates:
Image Received → CRC Check → Signature Verify → Reboot → Health Check → Mark Valid
│ │ │ │
│ FAIL: reject FAIL: reject FAIL: rollbackLayer 1: Transfer integrity. Compute SHA-256 (or at minimum CRC32) over the entire received image and compare it against the hash sent in the initial metadata packet. This catches corrupt transfers, BLE bit errors, and truncated images. Do this immediately after the last chunk is written. If it fails, erase the inactive slot and report the error. Never attempt to boot an image that fails integrity checks.
Layer 2: Authenticity via signature verification. Integrity tells you the image isn’t corrupt. Signature verification tells you it came from you. Sign every firmware binary at build time with your private key. Embed the corresponding public key in the device. Before marking the image as bootable, verify the signature. ESP-IDF’s Secure Boot v2 handles this at the bootloader level. Without this layer, anyone within BLE range could push arbitrary code to your devices.
Layer 3: Runtime health check. The image is intact and authentic, but does it actually work? After rebooting into the new slot, your application runs self-diagnostics: Does the BLE stack initialize? Do the sensors respond? Can it read NVS? Does the main task loop execute? Only after passing these checks should you call esp_ota_mark_valid_version(). Set a hardware watchdog with a timeout (say, 30 seconds). If the new firmware hangs or crashes before self-validating, the watchdog resets the device, the bootloader sees the image was never confirmed, and it rolls back automatically.
This three-layer approach is what separates a production OTA system from a demo. Each layer catches a different class of failure.
OTA Failure Recovery and Automatic Rollback
Rollback is the entire reason the A/B architecture exists. Here’s what triggers it:
- Watchdog timeout: new firmware never calls the validation function.
- Explicit rejection: new firmware detects a problem and calls
esp_ota_mark_invalid_version(). - Crash loop: bootloader tracks boot attempts; if the count exceeds a threshold without validation, it reverts.
The result in every case: the device reboots into the previous, known-good firmware. The failure mode is “device runs old firmware,” not “device is bricked.” This is the fundamental property you’re designing for.
Edge cases that are non-issues by design:
Power loss during flash write. The inactive slot has a partially written image. On next boot, the bootloader boots the active (good) slot as usual. The incomplete image is harmlessly overwritten on the next OTA attempt.
Power loss during otadata update. ESP-IDF uses a CRC-protected, dual-entry scheme for otadata, effectively making the swap atomic. If one entry is corrupt, the bootloader falls back to the other.
Anti-rollback protection is the other side of the coin. Once you’ve patched a security vulnerability, you may want to prevent downgrading to the vulnerable version. ESP-IDF supports eFuse-based hardware version counters for this. But be careful: burning an eFuse is permanent. If you increment the anti-rollback counter and then discover a bug in the new firmware, you cannot roll back past that counter value. Use this deliberately, not automatically.
Track OTA failure counts on the device. If a particular update fails three times, stop retrying and report status. Infinite retry loops waste battery and frustrate users.
End-to-End Considerations Beyond the Device
The device-side architecture is the foundation, but a complete OTA system extends further:
Versioning. Use semantic versioning baked into the firmware binary. Report the running version over a BLE characteristic so the companion app (or backend) knows what each device is running.
Backend infrastructure. Host signed firmware binaries, track which devices run which versions, and support staged rollouts. Push to 1% of devices first, monitor, then expand. This catches problems that testing didn’t.
Companion app. It handles BLE connection management, chunk transfers, progress UI, and error reporting. Don’t underestimate the engineering here; phone BLE stacks have their own quirks.
Monitoring. Devices should report OTA outcomes: success, rollback, failure reason. If 5% of your fleet rolls back after an update, you want to know immediately, not after customer complaints.
Building This Into Your Architecture Before You Ship
The architecture fits together as a chain: A/B partitions → safe bootloader with rollback → chunked BLE transfer with resume → integrity check → signature verification → runtime health check → mark valid or revert.
Every link exists to catch a specific failure mode. Remove one, and you have a gap that will eventually brick a device.
The most important thing you can do right now: implement and test rollback before you ship your first device. Flash deliberately broken firmware. Pull the power mid-transfer. Kill the BLE connection at random points. Verify that the device always comes back to life. If it doesn’t, you’ve found the gap, and you’ve found it on your bench instead of in 10,000 homes.
OTA isn’t about pushing new features to your devices. It’s about building enough safety into the update path that you trust it with hardware you’ll never touch again.
Hubble Network enables BLE firmware updates to devices anywhere on Earth—without range constraints or local gateway infrastructure. See how it works →