ESP32 OTA Update Tutorial: How to Update Firmware Wirelessly Using Arduino IDE (2026 Guide)
By Malik Hassan | | ⏱ 10 min read | 📚 Programming Tutorials
Updating ESP32 firmware with a USB cable every time is fine on a workbench — but what happens when your board is already sealed inside a junction box, mounted on a ceiling, or deployed across a fleet of ten remote nodes? Crawling under a desk or climbing a ladder for every firmware change is not a workflow. It is a problem.
ESP32 OTA (Over-The-Air) updates solve this entirely. Once you upload an OTA-enabled sketch once via USB, every future firmware version can be pushed wirelessly over Wi-Fi — no cable, no physical access. In this 2026 guide, I will walk you through three battle-tested OTA methods for ESP32, each with complete, copy-ready code I have personally tested on the DevKit V1, ESP32-S3, and ESP32-C6.
- Method 1 — ArduinoOTA: push firmware from Arduino IDE wirelessly
- Method 2 — HTTP OTA: ESP32 pulls a .bin file from a URL on boot
- Method 3 — Securing OTA with password and HTTPS
- OTA + Deep Sleep: the wake-window pattern for battery devices
- Troubleshooting the 5 most common OTA errors, explained clearly
What Is an ESP32 OTA Update?
OTA (Over-The-Air) update is the process of sending new firmware to a microcontroller over a network connection instead of a physical USB/serial cable. On ESP32, OTA is supported natively at the hardware level — Espressif's flash partition scheme reserves dedicated OTA_0 and OTA_1 slots in flash memory specifically for this purpose.
When an OTA update arrives, the ESP32 writes the new firmware image to the inactive partition while continuing to run the current code. Once the transfer is complete and verified, it reboots and boots from the new partition. If anything goes wrong mid-transfer, the ESP32 falls back to the last known-good firmware automatically. This dual-partition safety net is what makes OTA reliable enough for production deployments.
The Three Main OTA Methods on ESP32
- ArduinoOTA — Arduino IDE pushes firmware directly to the ESP32 over local WiFi. Best for active development.
- HTTP OTA (HTTPUpdate) — The ESP32 itself connects to a URL and pulls a .bin file. Best for deployed devices and fleet updates.
- Custom OTA Server — A full update server with version negotiation, rollback, HTTPS, and signed binaries. Best for commercial IoT products.
This guide covers Methods 1 and 2 in full. Method 3 builds on both and is explored briefly in the security section.
Prerequisites
- Hardware: Any ESP32 board (DevKit V1, FireBee, ESP32-S3, ESP32-C6, etc.)
- Arduino IDE 2.3.x or newer — download from arduino.cc
- ESP32 board package v3.x by Espressif Systems — install via Boards Manager
- ArduinoOTA library — already bundled with the ESP32 board package, no manual install
- Network: Your PC and ESP32 must be on the same local WiFi network
If this is your first time connecting ESP32 to WiFi, read my ESP32 WiFi Example guide first, then return here. OTA is built on top of a stable WiFi connection.
Method 1: ArduinoOTA — Push Firmware from Arduino IDE (Best for Development)
ArduinoOTA starts an mDNS (multicast DNS) service on the ESP32. This broadcasts the board's hostname on the local network, making it discoverable by Arduino IDE as a "network port." Once you select that port, you upload exactly as you normally would — the IDE just sends the compiled binary over WiFi instead of USB.
Step 1: Upload the OTA Bootstrap Sketch (USB — one time only)
This is the only time you need a cable. After this upload, all future firmware goes wirelessly. Replace YOUR_SSID and YOUR_WIFI_PASSWORD with your actual network credentials.
// ─────────────────────────────────────────────────────
// ESP32 ArduinoOTA Bootstrap Sketch | xloge.site 2026
// Compatible: ESP32 Arduino Core v3.x
// ─────────────────────────────────────────────────────
#include <WiFi.h>
#include <ArduinoOTA.h>
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
void setup() {
Serial.begin(115200);
// Step 1: Connect to WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(400);
Serial.print(".");
}
Serial.println();
Serial.print("Connected! IP: ");
Serial.println(WiFi.localIP());
// Step 2: Configure ArduinoOTA
ArduinoOTA.setHostname("xloge-esp32"); // Shows in Arduino IDE port list
ArduinoOTA.setPassword("xloge2026"); // Remove in hobby use, keep in shared spaces
ArduinoOTA.onStart([]() {
String type = (ArduinoOTA.getCommand() == U_FLASH) ? "sketch" : "filesystem";
Serial.println("OTA start — updating: " + type);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nOTA complete. Rebooting...");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.printf("OTA Error [%u]: ", error);
if (error == OTA_AUTH_ERROR) Serial.println("Auth failed — wrong password?");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin failed — check partition scheme");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect failed — network issue?");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive failed — unstable WiFi?");
else if (error == OTA_END_ERROR) Serial.println("End failed — incomplete transfer");
});
// Step 3: Start OTA listener
ArduinoOTA.begin();
Serial.println("OTA ready. Listening on port 3232...");
}
void loop() {
// CRITICAL: This must be called in every loop iteration
ArduinoOTA.handle();
// ── Your application code below this line ──────────────
// Keep delay() values small (under 100ms).
// Large delays block the OTA listener and cause timeouts.
delay(10);
}
Step 2: Confirm WiFi Connection in Serial Monitor
Open the Serial Monitor at 115200 baud. You should see:
Connecting to WiFi.... Connected! IP: 192.168.1.45 OTA ready. Listening on port 3232...
Note your ESP32's IP address — you may need it for troubleshooting.
Step 3: Select the Network Port in Arduino IDE
- Go to Tools → Port
- Wait 5–10 seconds for the port list to refresh
- Look for:
xloge-esp32 at 192.168.1.45 (esp32) - Select it
Step 4: Upload Wirelessly — All Future Updates
- Disconnect the USB cable (optional, but proves OTA works)
- Select the
xloge-esp32network port - Modify your sketch as needed
- Click the Upload arrow
- If prompted, enter the OTA password:
xloge2026 - Watch the progress percentage in the Serial Monitor
ArduinoOTA.handle() in loop(). If you accidentally upload a sketch without it, you will lose OTA access permanently and must re-upload via USB.Method 2: HTTP OTA — ESP32 Pulls Firmware from a URL (Best for Production)
HTTP OTA flips the direction of the update. Instead of Arduino IDE pushing firmware to the ESP32, the ESP32 itself checks a URL and pulls the compiled .bin file. This is the pattern used by commercial IoT products: your fleet of devices checks a server on each boot, compares firmware versions, and downloads an update only when one is available.
This approach scales to hundreds of devices with zero manual intervention per device.
Step 1: Export Your Compiled Binary
In Arduino IDE 2.x: Sketch → Export Compiled Binary. A .bin file appears in your sketch folder. Upload this file to any web-accessible location:
- GitHub: Push to a public repo, use the raw URL (
raw.githubusercontent.com/...) - Your own web hosting: Upload via FTP/cPanel and serve directly
- Local ngrok tunnel: For testing — tunnels your PC's localhost to a public URL
Step 2: HTTP OTA Sketch
// ─────────────────────────────────────────────────────
// ESP32 HTTP OTA Update Sketch | xloge.site 2026
// ESP32 pulls firmware from a URL on every boot
// ─────────────────────────────────────────────────────
#include <WiFi.h>
#include <HTTPClient.h>
#include <HTTPUpdate.h>
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* firmware_url = "http://your-server.com/firmware/esp32_ota_v2.bin";
// ↑ Replace with your actual .bin URL
WiFiClient client;
void checkAndApplyOTAUpdate() {
Serial.println("Checking for OTA update at:");
Serial.println(firmware_url);
httpUpdate.onStart([]() { Serial.println("HTTP OTA: Starting download..."); });
httpUpdate.onEnd([]() { Serial.println("HTTP OTA: Download complete."); });
httpUpdate.onProgress([](int current, int total) {
Serial.printf("HTTP OTA: %d / %d bytes\n", current, total);
});
t_httpUpdate_return result = httpUpdate.update(client, firmware_url);
switch (result) {
case HTTP_UPDATE_OK:
// ESP32 auto-reboots after a successful update
// Code below this line does not execute after update
Serial.println("Update applied. Rebooting into new firmware...");
break;
case HTTP_UPDATE_NO_UPDATES:
Serial.println("Server returned: no update available. Running current firmware.");
break;
case HTTP_UPDATE_FAILED:
Serial.printf("Update FAILED. Error %d: %s\n",
httpUpdate.getLastError(),
httpUpdate.getLastErrorString().c_str());
break;
}
}
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(400);
Serial.print(".");
}
Serial.println("\nConnected!");
checkAndApplyOTAUpdate(); // Check for update on every boot
}
void loop() {
// Main application logic runs here after the update check
// If an update was applied, this code is from the NEW firmware
delay(1000);
}
The ESP32 reboots automatically after a successful HTTP OTA update. On the next boot, it runs the new firmware and checks for updates again — completing a perpetual, self-managing update cycle.
Securing Your ESP32 OTA Updates
Unsecured OTA is one of the most common attack vectors in IoT systems. Anyone on the same local network could push arbitrary firmware to your ESP32. Here are the three layers of protection to apply:
Layer 1: ArduinoOTA Password
Already covered above: ArduinoOTA.setPassword("yourpassword"). Use a strong, unique string. This protects development OTA but offers no encryption — the password is transmitted in plain text, so it is not enough for production.
Layer 2: HTTPS for HTTP OTA
Swap WiFiClient for WiFiClientSecure to encrypt the firmware download:
#include <WiFiClientSecure.h>
WiFiClientSecure secureClient;
// For testing only — bypasses certificate verification:
secureClient.setInsecure();
// For production — pin your server certificate:
// secureClient.setCACert(rootCACertificate); // Your CA cert as PEM string
t_httpUpdate_return result = httpUpdate.update(
secureClient,
"https://your-server.com/firmware/esp32_ota_v2.bin"
);
For a complete walkthrough of certificate management and TLS on microcontrollers, see my guide on Securing IoT Telemetry with TLS/SSL on Microcontrollers.
Layer 3: Firmware Signature Verification (Advanced)
The ESP32 Arduino Core v3.x supports signed binary updates via RSA or ECDSA. This cryptographically verifies that the firmware came from you and has not been tampered with. This is the recommended approach for any commercial or security-critical product.
OTA + Deep Sleep: The Wake-Window Pattern
Battery-powered ESP32 sensors — temperature monitors, moisture sensors, remote cameras — use deep sleep to sip power. The challenge: deep sleep shuts down WiFi completely, so the OTA listener cannot receive updates during sleep.
The standard solution is the OTA window pattern. On each wake-up, before taking sensor readings or going back to sleep, the ESP32 briefly connects to WiFi and listens for OTA updates for a short, fixed window (typically 10–15 seconds). If an update arrives, it applies it. If not, it proceeds with its normal tasks and goes back to sleep.
#include <WiFi.h>
#include <ArduinoOTA.h>
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
#define OTA_WINDOW_MS 10000 // 10-second OTA window per wake cycle
#define SLEEP_SECONDS 30 // Deep sleep duration between readings
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
// Wait up to 8 seconds for WiFi (skip OTA window if no connection)
unsigned long wifiStart = millis();
while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 8000) {
delay(200);
}
if (WiFi.status() == WL_CONNECTED) {
ArduinoOTA.setHostname("xloge-sensor");
ArduinoOTA.setPassword("xloge2026");
ArduinoOTA.begin();
// ── OTA listen window ──────────────────────────────
unsigned long otaStart = millis();
while (millis() - otaStart < OTA_WINDOW_MS) {
ArduinoOTA.handle();
delay(10);
}
Serial.println("OTA window closed — proceeding.");
}
// ── Your sensor / data logic here ─────────────────────
// readSensor();
// sendToMQTT();
// ── Return to deep sleep ───────────────────────────────
Serial.printf("Sleeping for %d seconds...\n", SLEEP_SECONDS);
esp_deep_sleep((uint64_t)SLEEP_SECONDS * 1000000ULL);
}
void loop() {
// Never reached when using deep sleep
}
On a typical LiPo-powered node, this pattern adds only 10 seconds of active WiFi time per wake cycle. For a device waking every 30 seconds, that is still over 70% sleep duty cycle — easily months of battery life depending on the sensor load.
Troubleshooting ESP32 OTA: 5 Common Errors Solved
1. OTA network port does not appear in Arduino IDE
This is the most frequently asked OTA question. The network port appears only when: (a) the ESP32 is powered and running the OTA sketch, (b) it is connected to WiFi on the same subnet as your PC, and (c) ArduinoOTA.handle() is being called in loop(). To fix: restart Arduino IDE, verify the Serial Monitor shows "OTA ready," and confirm your PC is on the same router. On Windows, mDNS discovery requires the Bonjour service — install it from Apple's site if needed.
2. "No response from device" or upload times out
Usually a firewall or WiFi instability issue. Check: (1) Is UDP port 3232 blocked by Windows Defender Firewall? Temporarily disable it to test. (2) Is your ESP32 loop() calling large delay() values (over 200ms) that prevent handle() from running fast enough? Reduce delay values in your sketch.
3. "OTA_AUTH_ERROR" — authentication failed
The OTA password on the ESP32 does not match what Arduino IDE sent. Verify the password in ArduinoOTA.setPassword() exactly (case-sensitive) and re-enter it when the IDE prompts. Clear IDE's saved OTA password if needed by selecting a different port and back.
4. "OTA_BEGIN_ERROR" — begin failed
Your compiled sketch is too large for the OTA partition. Go to Tools → Partition Scheme and select a partition that includes OTA space (e.g., "Default 4MB with SPIFFS" or "Minimal SPIFFS"). Never use a "No OTA" partition scheme if you intend to use OTA.
5. HTTP OTA returns HTTP_UPDATE_FAILED
Check the error code printed to Serial. Common causes: (a) Wrong URL — test the URL in your browser; it should prompt a .bin file download. (b) Server returns 404 — check the file path. (c) Server sends wrong Content-Type — it must be application/octet-stream. (d) SSL certificate issue — use setInsecure() for testing, then add proper certs for production.
Frequently Asked Questions
What port does ESP32 OTA use?
ArduinoOTA uses UDP port 3232 by default. You can change it with ArduinoOTA.setPort(8266) if needed. Ensure your local network does not block this port. Enabling WiFi on Windows Firewall during OTA uploads usually resolves port-related issues.
Can I run OTA updates while ESP32 is in deep sleep?
Not during sleep itself — deep sleep disables WiFi. Use the OTA window pattern shown above: wake up, listen for OTA for 10 seconds, then sleep. This gives you a reliable update path without sacrificing meaningful battery life.
How do I add password protection to ESP32 OTA?
Add ArduinoOTA.setPassword("yourpassword") before ArduinoOTA.begin(). Arduino IDE will prompt for the password on the first OTA upload from each new computer. For production devices, combine this with HTTPS OTA instead of relying on password-only ArduinoOTA.
Does ArduinoOTA work on ESP32-S3 and ESP32-C6?
Yes — the OTA code shown in this guide works across the entire ESP32 family: classic ESP32, S2, S3, C3, C6, and H2. I have tested it personally on the ESP32-S3 and ESP32-C6. Only the board selection in Arduino IDE changes.
How much flash does OTA require?
OTA requires approximately double your sketch size in flash, because the new firmware is written to an inactive partition while the current one runs. On a 4MB ESP32, the default OTA partition scheme allocates about 1.4 MB per OTA slot — enough for most projects. If your sketch exceeds this, choose a larger OTA partition scheme under Tools → Partition Scheme.
Conclusion: Make Every ESP32 Project Updatable from Day One
OTA updates are not an advanced feature — they are a baseline requirement for any ESP32 project that will be deployed in the real world. Whether you are iterating on firmware in your lab or managing a fleet of remote sensors, OTA eliminates the physical dependency on USB access and lets you ship improvements, security patches, and new features as fast as you can write them.
Here are the key rules to remember:
- Every sketch must include
ArduinoOTA.handle()inloop()— or OTA access is lost - Use
setPassword()in development and HTTPS + certificate pinning in production - For battery-powered devices, pair OTA with the wake-window pattern alongside deep sleep
- Choose a partition scheme that includes OTA space — never "Huge App (No OTA)" unless you are certain
For a production-ready IoT system that combines OTA, MQTT, and Edge AI inference, see my guide: Edge AI with ESP32: Production-Grade IoT Systems Guide 2026.
Questions about a specific error you're hitting? Drop them in the comments — I read and respond to every one.
Malik Hassan is an embedded systems engineer and the creator of xloge.site. He specialises in ESP32, TinyML, and production IoT deployments. He has built and shipped OTA update pipelines for smart home automation systems, environmental monitoring networks, and edge AI inference devices running on the ESP32-S3 and ESP32-C6.
