Phase 4: Connectivity 14 min read

OTA Updates on ESP32 — Update Firmware Over Wi-Fi

Flash new firmware to ESP32 over Wi-Fi without a USB cable. Covers ArduinoOTA (Arduino IDE), web browser OTA with ESPAsyncWebServer, and secure HTTPS OTA from a server.

Updated June 18, 2026

Why OTA Matters

Once an ESP32 sensor is mounted behind a wall, embedded in a product enclosure, or deployed to a remote location, reprogramming it by USB cable is impractical. OTA lets you push bug fixes and new features wirelessly from your laptop — or even automate firmware updates from a cloud server.

Method 1 — ArduinoOTA (Arduino IDE Upload via Network)

The simplest approach: the ESP32 appears as a network port in the Arduino IDE. Select it, click Upload, and the IDE sends the firmware over Wi-Fi.

ota_arduino.ino
#include <WiFi.h>
#include <ArduinoOTA.h>

const char* ssid     = "YourSSID";
const char* password = "YourPassword";

void setup() {
  Serial.begin(115200);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println("\nIP: " + WiFi.localIP().toString());

  ArduinoOTA.setHostname("esp32-sensor");    // appears as this in IDE
  ArduinoOTA.setPassword("ota-password");    // optional but recommended

  ArduinoOTA.onStart([]() {
    String type = (ArduinoOTA.getCommand() == U_FLASH) ? "firmware" : "filesystem";
    Serial.println("OTA start: " + type);
  });
  ArduinoOTA.onEnd([]()   { Serial.println("\nOTA done — rebooting"); });
  ArduinoOTA.onError([](ota_error_t e) {
    Serial.printf("OTA error[%u]: ", e);
    if      (e == OTA_AUTH_ERROR)    Serial.println("Auth failed");
    else if (e == OTA_BEGIN_ERROR)   Serial.println("Begin failed");
    else if (e == OTA_CONNECT_ERROR) Serial.println("Connect failed");
    else if (e == OTA_RECEIVE_ERROR) Serial.println("Receive failed");
    else if (e == OTA_END_ERROR)     Serial.println("End failed");
  });
  ArduinoOTA.onProgress([](unsigned int prog, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (prog / (total / 100)));
  });

  ArduinoOTA.begin();
  Serial.println("ArduinoOTA ready — select 'esp32-sensor' in IDE > Tools > Port");
}

void loop() {
  ArduinoOTA.handle();   // must call every loop — checks for incoming update
  // your application code here
}

Method 2 — Web Browser OTA (Upload .bin via HTML Form)

No Arduino IDE required — upload firmware from any web browser by browsing to the ESP32’s IP and selecting the .bin file.

ota_web.ino
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Update.h>

const char* ssid     = "YourSSID";
const char* password = "YourPassword";
AsyncWebServer server(80);

const char OTA_PAGE[] PROGMEM = R"(
<!DOCTYPE html><html><head><title>OTA Update</title></head><body>
<h1>OTA Firmware Update</h1>
<form method='POST' action='/update' enctype='multipart/form-data'>
  <input type='file' name='firmware' accept='.bin'>
  <input type='submit' value='Update'>
</form>
</body></html>
)";

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println("\nIP: " + WiFi.localIP().toString());

  server.on("/", HTTP_GET, [](AsyncWebServerRequest* req) {
    req->send_P(200, "text/html", OTA_PAGE);
  });

  server.on("/update", HTTP_POST,
    [](AsyncWebServerRequest* req) {
      bool ok = !Update.hasError();
      AsyncWebServerResponse* response = req->beginResponse(
        200, "text/plain", ok ? "Update OK — rebooting" : "Update FAILED");
      response->addHeader("Connection", "close");
      req->send(response);
      if (ok) ESP.restart();
    },
    [](AsyncWebServerRequest* req, String filename, size_t index,
       uint8_t* data, size_t len, bool final) {
      if (!index) {
        Serial.printf("OTA start: %s\n", filename.c_str());
        if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
          Update.printError(Serial);
        }
      }
      if (Update.write(data, len) != len) Update.printError(Serial);
      if (final) {
        if (Update.end(true)) {
          Serial.printf("OTA complete: %u bytes\n", index + len);
        } else {
          Update.printError(Serial);
        }
      }
    }
  );

  server.begin();
  Serial.println("Web OTA server ready at http://" + WiFi.localIP().toString());
}

void loop() {}

Method 3 — Automatic OTA from HTTP Server

The ESP32 periodically checks a version file on your server and downloads the firmware binary if a newer version is available:

ota_auto.ino
#include <WiFi.h>
#include <HTTPClient.h>
#include <Update.h>

#define CURRENT_VERSION "1.0.2"
#define VERSION_URL     "http://your-server.com/esp32/version.txt"
#define FIRMWARE_URL    "http://your-server.com/esp32/firmware.bin"

void checkForUpdate() {
  HTTPClient http;
  http.begin(VERSION_URL);
  if (http.GET() == HTTP_CODE_OK) {
    String latest = http.getString();
    latest.trim();
    if (latest != CURRENT_VERSION) {
      Serial.printf("New version %s available (have %s) — downloading\n",
        latest.c_str(), CURRENT_VERSION);
      downloadAndApply();
    } else {
      Serial.println("Firmware up to date: " + String(CURRENT_VERSION));
    }
  }
  http.end();
}

void downloadAndApply() {
  HTTPClient http;
  http.begin(FIRMWARE_URL);
  int code = http.GET();
  if (code == HTTP_CODE_OK) {
    int total = http.getSize();
    if (!Update.begin(total > 0 ? total : UPDATE_SIZE_UNKNOWN)) {
      Update.printError(Serial); return;
    }
    WiFiClient* stream = http.getStreamPtr();
    size_t written = Update.writeStream(*stream);
    if (Update.end()) {
      Serial.printf("Updated: %u bytes — rebooting\n", written);
      ESP.restart();
    } else {
      Update.printError(Serial);
    }
  }
  http.end();
}

void loop() {
  static unsigned long last = 0;
  if (millis() - last >= 3600000) {   // check every hour
    last = millis();
    checkForUpdate();
  }
  // your application code
}

Partition Scheme for OTA

OTA requires the correct partition scheme. In Arduino IDE: Tools → Partition Scheme → select one with “OTA” in the name (e.g. “Default with OTA (1.3 MB APP / 1.5 MB SPIFFS)”).

Rollback Safety Pattern

ota_rollback.ino
#include <esp_ota_ops.h>

// In setup(), after all hardware initialised successfully:
esp_ota_img_states_t state;
const esp_partition_t* running = esp_ota_get_running_partition();
if (esp_ota_get_state_partition(running, &state) == ESP_OK) {
  if (state == ESP_OTA_IMG_PENDING_VERIFY) {
    // Mark this boot as valid — do NOT call this if something is wrong
    esp_ota_mark_app_valid_cancel_rollback();
    Serial.println("OTA: firmware marked as valid");
  }
}
// If setup() crashes before reaching this line, the watchdog timer
// triggers a reboot and the bootloader rolls back to the previous image.

Frequently Asked Questions

OTA lets you upload new firmware to ESP32 over Wi-Fi without connecting a USB cable. The ESP32 runs the new firmware from a second flash partition (OTA partition 0 and OTA partition 1) and switches to it on reboot, leaving the old firmware as a rollback option.
OTA needs two firmware partitions plus one OTA data partition. By default each firmware partition is about half the available flash minus overhead. On a 4 MB flash chip, each OTA slot is about 1.9 MB, leaving ~1.8 MB for SPIFFS/LittleFS.
ArduinoOTA uses the Arduino IDE's built-in upload mechanism over mDNS and UDP — you select the network port in the IDE exactly like a USB port. Web-browser OTA uses an HTTP endpoint where you upload the .bin file through a web form without the Arduino IDE.
By default ArduinoOTA has optional MD5 hash verification and password protection, but it sends the firmware unencrypted over the local network. For production devices, use HTTPS OTA (Update library with WiFiClientSecure) to encrypt the firmware in transit.
Yes. The esp_ota_ops.h API includes esp_ota_mark_app_invalid_rollback_and_reboot() to return to the previous firmware partition. Implement a "healthy boot" check in your app_main and only mark the update as valid after verifying the new firmware works correctly.
Use the Update library with HTTPClient: periodically check a version endpoint, compare with your current version (defined as a build flag), and if a newer version is available call Update.begin() followed by Update.writeStream(httpStream) to download and apply the update.
Yes. Arduino IDE OTA supports a separate filesystem upload. Build with Sketch → Export Compiled Binary, then use the filesystem image OTA slot. Alternatively upload files individually via HTTP PUT endpoints served by the ESP32.
If the write to flash fails mid-way, the OTA data partition still points to the old firmware and the ESP32 boots normally after a timeout or power cycle. The incomplete new firmware partition is simply overwritten on the next update attempt.
Call ArduinoOTA.setPassword("yourpassword") before ArduinoOTA.begin(). The Arduino IDE will prompt for the password when uploading via the network port. Use a strong password — the credentials are sent as an MD5 hash, not plain text.
Yes. The Update library's Update.begin() / Update.write() / Update.end() functions work independently of ArduinoOTA. Build your firmware binary in Arduino IDE (Sketch → Export Compiled Binary) and upload it via any HTTP server endpoint you create.

Projects to Build

Put this knowledge to work — try one of these hands-on projects.