The Verdict: Which ESP32 OTA Method to Choose
When deploying ESP32 firmware over the air, you have three primary paths. Choosing the wrong one leads to bricked devices or impossible maintenance cycles. Here is the decision matrix to lock in your architecture.
| Method | Best For | Network Requirement | Server Setup |
|---|---|---|---|
| HTTPUpdate | Deployed fleets, remote devices, production | WAN / Internet (HTTP/HTTPS) | Standard web server (Nginx, Apache, S3) |
| ArduinoOTA | Bench development, local LAN debugging | LAN only (mDNS) | None (uses Python script on host PC) |
| ESP-IDF Native | Bare-metal C/C++, complex rollback logic | WAN / Internet | Custom C implementation required |
HTTPUpdate. It requires zero specialized server software beyond a basic static file host, works through standard NAT/firewalls, and integrates cleanly with the Arduino ESP32 Core 3.x API.
Hardware & Parts List: ESP32 HTTPUpdate Setup
While HTTPUpdate is a software library, a robust physical build requires visual feedback and a manual override trigger. This parts list targets the most common development board on the market.
| Component | Specification / Model | Purpose |
|---|---|---|
| Microcontroller | ESP32-WROOM-32 DevKit V1 (30-pin) | Main target board (4MB Flash) |
| Status LED | 5mm Green LED + 330Ω Resistor | Visual OTA progress indicator |
| Trigger Switch | 6x6mm Tactile Pushbutton | Force-update override (bypasses timer) |
| Power Supply | 5V 2A USB-C or Micro-USB cable | Prevents brownouts during flash write |
Pin Mapping Table
| GPIO Pin | Component | Configuration |
|---|---|---|
| GPIO 2 | Status LED (via 330Ω resistor) | OUTPUT (Active HIGH) |
| GPIO 0 | Force Update Button | INPUT_PULLUP (Active LOW) |
Complete ESP32 HTTPUpdate Code (Arduino Core 3.x)
This code targets the ESP32 Dev Module board definition. It connects to WiFi, checks for a forced update via the GPIO 0 button, and executes the HTTP update with full error handling. It explicitly disables auto-reboot so you can execute cleanup tasks (like saving state to NVS) before restarting.
#include <WiFi.h>
#include <HTTPUpdate.h>
// --- PIN DEFINITIONS ---
#define STATUS_LED 2
#define FORCE_UPDATE_BTN 0
// --- NETWORK & SERVER CONFIG ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* fw_url = "http://192.168.1.100/firmware/v2.bin";
// Update check interval (1 hour)
const unsigned long updateInterval = 3600000;
unsigned long lastUpdateCheck = 0;
void setup() {
Serial.begin(115200);
pinMode(STATUS_LED, OUTPUT);
pinMode(FORCE_UPDATE_BTN, INPUT_PULLUP);
digitalWrite(STATUS_LED, LOW);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected. IP: " + WiFi.localIP().toString());
}
void loop() {
// Check for forced button press OR scheduled interval
bool buttonPressed = (digitalRead(FORCE_UPDATE_BTN) == LOW);
bool timeElapsed = (millis() - lastUpdateCheck > updateInterval);
if (buttonPressed || timeElapsed) {
lastUpdateCheck = millis();
performOTAUpdate();
}
delay(100); // Yield to WiFi task
}
void performOTAUpdate() {
Serial.println("[OTA] Starting HTTPUpdate...");
digitalWrite(STATUS_LED, HIGH); // LED ON during update
WiFiClient client;
HTTPUpdate httpUpdate;
// Configure HTTPUpdate behavior
httpUpdate.setLedPin(-1); // We handle LED manually
httpUpdate.rebootOnUpdate(false); // CRITICAL: Manual reboot for safety
httpUpdate.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
t_httpUpdate_return ret = httpUpdate.update(client, fw_url);
switch(ret) {
case HTTP_UPDATE_FAILED:
Serial.printf("[OTA] Error (%d): %s\n",
httpUpdate.getLastError(),
httpUpdate.getLastErrorString().c_str());
break;
case HTTP_UPDATE_NO_UPDATES:
Serial.println("[OTA] Server returned 304 Not Modified.");
break;
case HTTP_UPDATE_OK:
Serial.println("[OTA] Success. Rebooting...");
// Blink LED to indicate success before reboot
for(int i=0; i<5; i++) {
digitalWrite(STATUS_LED, !digitalRead(STATUS_LED));
delay(100);
}
ESP.restart();
break;
}
digitalWrite(STATUS_LED, LOW); // LED OFF on failure/no-update
}
Troubleshooting: Exact Error Strings and Ranked Causes
When HTTP_UPDATE_FAILED triggers, the getLastErrorString() method returns specific strings. Here is the exact translation of those strings and how to fix them.
1. Error String: "HTTP_UPDATE_NO_SPACE"
- Rank 1 (95%): Incorrect Partition Table. You are using the "Default 4MB with spiffs" scheme, which allocates one massive 3MB app partition. OTA requires two app partitions (Active and Standby).
- Fix: In the Arduino IDE, go to Tools > Partition Scheme and select "Default 4MB with OTA (1.2MB APP/1.5MB SPIFFS)" or "Minimal OTA (1.9MB APP/190KB SPIFFS)".
2. Error String: "Connection refused" or "Connect failed"
- Rank 1 (60%): The web server is down, or the IP address in
fw_urlis incorrect. - Rank 2 (30%): Your router's AP Isolation (Guest Network) is blocking the ESP32 from talking to your local server.
- Rank 3 (10%): The ESP32 hasn't fully obtained an IP address before the update function fires (always check
WiFi.status() == WL_CONNECTEDfirst).
3. Error String: "Write to flash failed" or "Failed to write chunk"
- Rank 1 (80%): Insufficient contiguous heap memory. The ESP32 needs a large, unfragmented block of RAM to buffer the incoming binary chunks before writing to flash.
- Fix: Call
ESP.getFreeHeap()andheap_caps_get_largest_free_block(MALLOC_CAP_8BIT)before triggering the update. If the largest block is under 100KB, close open files, disconnect MQTT, or free dynamically allocated buffers before callinghttpUpdate.update(). - Rank 2 (20%): Flash chip degradation or a counterfeit ESP32 module with mismatched flash size.
The First Three Things to Check When OTA Fails
If your serial monitor just spits out a generic failure and you are stuck, run through this mandatory three-point checklist before rewriting your code.
This is the #1 killer of ESP32 OTA projects. Open your Arduino IDE or PlatformIO
platformio.ini. If your partition table does not explicitly contain ota_0 and ota_1 labels, HTTPUpdate will instantly fail with a space error. In PlatformIO, add board_build.partitions = default_4MB.csv or min_ota.csv.
Your web server must serve the
.bin file cleanly. If your server forces an HTTP to HTTPS redirect, or redirects http://domain.com/firmware.bin to a download page, the ESP32's basic HTTPClient will choke. Ensure your server returns a direct 200 OK with the Content-Type: application/octet-stream header. In the code above, setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS) helps, but direct links are safer.
Writing to flash while simultaneously pulling data over WiFi causes massive current spikes (up to 500mA). If you are powering the DevKit via a weak laptop USB port or a 500mA wall wart, the voltage will droop below 3.3V, causing the flash controller to reset mid-write. Always use a dedicated 5V 2A+ power supply for OTA testing.
Extending the Build: HTTPS and Rollbacks
Once you have basic HTTP updates working, production environments demand two upgrades: encrypted transport and safe rollbacks.
Upgrading to HTTPS
To use HTTPS, swap WiFiClient for WiFiClientSecure. You must provide the root CA certificate of your server. In Arduino Core 3.x, you can skip strict verification for internal servers using client.setInsecure(), but for public servers, embed the PEM certificate:
WiFiClientSecure client;
client.setCACert(root_ca_pem); // Your const char* PEM string
httpUpdate.update(client, "https://secure-server.com/fw.bin");
Note: TLS handshakes consume roughly 50KB-80KB of additional heap memory. Ensure you are using an ESP32 with PSRAM (like the ESP32-WROVER) if your application is already memory-heavy.
Implementing Safe Rollbacks
If your new firmware has a bug that prevents WiFi connection, the device will be bricked. The ESP32's native OTA API supports automatic rollbacks. To enable this, you must mark the new firmware as "valid" only after it successfully connects to the cloud.
According to the Espressif OTA Documentation, if you do not call esp_ota_mark_app_valid(), the bootloader will automatically revert to the previous partition on the next hard reset. Add this to your setup() function:
#include <esp_ota_ops.h>
void setup() {
// ... WiFi connection code ...
if (WiFi.status() == WL_CONNECTED) {
// Mark this firmware as good. If we crash before this,
// the bootloader rolls back to the old firmware next boot.
esp_ota_mark_app_valid();
}
}
For deeper integration and advanced HTTP client configurations, always refer to the official Arduino ESP32 HTTPUpdate examples repository to ensure compatibility with the latest Core releases.
Final Recommendation: Start with plain HTTP on a local Nginx server to validate your partition scheme and memory management. Once the HTTP_UPDATE_OK path is proven, migrate to HTTPS with esp_ota_mark_app_valid() rollback protection before deploying to the field.






