The 3 OTA Update Mistakes That Brick ESP32 Boards in Production
I've shipped OTA updates to thousands of ESP32 devices. Here are the three mistakes I see devs make that turn a routine firmware push into a fleet-wide disaster — and how to avoid each one.
1. Skipping the partition table dual-OTA scheme
Most tutorials show you a single factory partition and call it done. That works in your garage. In production, a failed OTA write with no fallback partition means your device is a paperweight.
The fix: Always use the ota_0 / ota_1 dual partition layout. Your bootloader alternates between them, so if a new firmware fails the integrity check, it rolls back to the last known-good image automatically.
# Partition table for dual OTA
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x4000
otadata, data, ota, 0xd000, 0x2000
phy_init, data, phy, 0xf000, 0x1000
ota_0, app, ota_0, 0x10000, 0x1E0000
ota_1, app, ota_1, 0x1F0000, 0x1E00002. Not validating firmware images before marking them valid
The ESP-IDF OTA API has a two-step commit: you write the image, then you call esp_ota_mark_app_valid_cancel_rollback(). If you skip this call, the bootloader treats the new firmware as "pending verification" — and if it reboots for any reason (watchdog, brownout, panic), it rolls back.
That sounds safe until you realize: if you DO call it immediately without testing anything, you've just committed to firmware that might be broken.
The fix: After OTA write completes, run a self-test routine before marking valid:
// After OTA flash, before marking valid:
bool self_test_passed = true;
// Test Wi-Fi connectivity
if (esp_wifi_start() != ESP_OK) self_test_passed = false;
// Test sensor readings
if (read_temperature() < -40 || read_temperature() > 125) self_test_passed = false;
// Test MQTT broker connection
if (mqtt_client_connect() != ESP_OK) self_test_passed = false;
if (self_test_passed) {
esp_ota_mark_app_valid_cancel_rollback();
ESP_LOGI(TAG, "OTA validated successfully");
} else {
ESP_LOGE(TAG, "Self-test failed, rebooting to rollback...");
esp_restart(); // Will boot into previous partition
}3. Pushing OTA updates without version gating
When you have devices in the field running firmware v1.2, v1.5, and v2.0, pushing a v3.0 update to all of them simultaneously is asking for trouble. Schema changes, API contract shifts, and NVS format migrations can all break older firmware that tries to jump multiple versions.
The fix: Implement version-aware OTA. Before downloading an update, the device reports its current version. Your server responds with the next safe version — not the latest.
// Device-side version check before OTA
#define CURRENT_FW_VERSION "1.5.0"
void check_for_update(void) {
char url[256];
snprintf(url, sizeof(url),
"https://api.yourserver.com/ota/next?current=%s&device=%s",
CURRENT_FW_VERSION, get_device_id());
// Server returns 204 No Content if already on latest
// Server returns 200 with binary URL for next safe version
// Device never skips versions
}This gives you a stepped migration path: v1.2 → v1.5 → v2.0 → v3.0, each step handling its own data migration safely.
---
These three patterns — dual partitions, validated commits, and version gating — are the difference between "OTA works on my bench" and "OTA works across 10,000 devices in the field."
If you want to go deeper on building production OTA pipelines with ESP-IDF and Expo EAS, that's exactly what we cover inside ESP32 Firmware Mastery.
