How to flash an ESP32 from your browser: offsets, drivers, the BOOT button, and what to do when it fails
You do not need to install anything to put firmware on an ESP32. Chrome and Edge can open a USB serial port directly, and Espressif's own flashing code runs as JavaScript, so a .bin file can go from your disk to the chip inside one browser tab. The ESP32 web flasher on this site does exactly that for any firmware image, free and with no account. What this guide adds is the part every flasher page skips: which file goes at which address, why the board sometimes needs its BOOT button held, which driver your particular board wants, and how to read the handful of error messages that account for almost every failed flash.
Why flash from a browser at all
The classic ways to program an ESP32 all start with an install: the Arduino IDE with its board package, PlatformIO inside VS Code, or Python plus esptool.py. They are fine tools when you are writing firmware. They are a lot of setup when all you want is to load a binary somebody else built, or to recover a board, or to flash a second machine that has none of it.
The browser route relies on two things. The Web Serial API lets a page open a serial port after you pick it in a permission prompt, and esptool-js is Espressif's esptool compiled for the browser, so the chip detection, the compression, the sector erases and the hash check are the same ones the command-line tool performs. Nothing is uploaded anywhere: the page reads the file locally and streams it to the chip.
| Arduino IDE | PlatformIO | esptool.py | Browser flasher | |
|---|---|---|---|---|
| Install | ~1 GB with the ESP32 core | VS Code + extension + toolchains | Python + pip install esptool |
Nothing |
Flashes a prebuilt .bin |
Awkward (it wants a sketch) | Awkward | Yes, with offsets typed by hand | Yes, offsets from a preset |
| Offsets handled for you | Yes, for its own builds | Yes, for its own builds | No | Preset or custom |
| Works on a locked-down machine | Rarely | Rarely | Sometimes | If Chrome or Edge runs |
| Browsers | n/a | n/a | n/a | Chrome, Edge, Opera on desktop |
Firefox and Safari do not implement Web Serial, and neither do phone browsers, so this is a desktop Chromium job. There is nothing to enable; the API is simply present in those browsers.
What you need
- The board, plugged in over a data USB cable. Charge-only cables are the single most common reason a port never appears. A direct port beats a hub.
- Chrome or Edge on Windows, macOS or Linux.
- The firmware files. Where they are depends on what built them:
- Arduino IDE 2: Sketch → Export Compiled Binary writes
build/<board>/inside the sketch folder, containing<sketch>.ino.bin(the application),.ino.bootloader.bin,.ino.partitions.binand, on recent cores,.ino.merged.bin(everything in one file). - PlatformIO:
.pio/build/<env>/firmware.bin,bootloader.binandpartitions.bin. Theboot_app0.binthat Arduino-style builds also need lives in the framework package at~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin. - ESP-IDF:
build/bootloader/bootloader.bin,build/partition_table/partition-table.binandbuild/<project>.bin. The build also writesbuild/flasher_args.json, which lists the exact offset for each file, so you never have to guess. - Someone else's release: usually a single merged or "factory" image that goes at
0x0, or an application image that goes at0x10000. The release notes normally say which.
- Arduino IDE 2: Sketch → Export Compiled Binary writes
Which file goes at which offset
The ESP32's ROM looks for a second-stage bootloader at a fixed address, the bootloader reads a partition table at 0x8000, and the partition table says where the application lives. Put a file at the wrong address and the board boots into nothing. The addresses differ by chip family, and this is the detail that catches people who move from a classic ESP32 to a newer part.
| File | ESP32 and ESP32-S2 | ESP32-S3, C3, C6, C2, H2 | ESP32-P4 |
|---|---|---|---|
| Bootloader | 0x1000 |
0x0 |
0x2000 |
| Partition table | 0x8000 |
0x8000 |
0x8000 |
OTA data (boot_app0.bin, Arduino builds) |
0xE000 |
0xE000 |
0xE000 |
Application (firmware.bin, .ino.bin) |
0x10000 |
0x10000 |
0x10000 |
| Merged or factory image | 0x0 |
0x0 |
0x0 |
The S2 is the one to watch: it is the newer chip that still uses 0x1000, exactly like the classic ESP32. Espressif's own esptool documentation is the reference if you are working with a part not listed here. An ESP8266 is simpler: its firmware is a single image at 0x0.
Three practical rules fall out of that table:
- Application only is enough on a board that has run before. The bootloader and partition table are already in flash from whatever was there last, so a new
firmware.binat0x10000is a complete update. This is the everyday case and the flasher's default preset. - A full set is needed on a blank chip, or when the new firmware uses a different partition layout. Then you write all four files, and the bootloader address must match the chip family.
- A merged image replaces both. Tools like
esptool merge_binand the Arduino export pack bootloader, table and app into one file laid out from address zero, which is why it always goes at0x0.
Offsets must sit on 4 KB sector boundaries because the erase happens per sector; the flasher warns you if one does not, since a misaligned write erases downward into the image below it.
Drivers and ports: making the board show up
The permission prompt only lists ports the operating system already knows about. If yours is missing, it is one of four things.
The USB-to-serial bridge needs a driver. Most dev boards carry a Silicon Labs CP210x or a WCH CH340 (sometimes CH343) chip between the USB connector and the ESP32's UART; a few use FTDI. macOS and current Windows and Linux ship drivers for all three, but older Windows installs and some CH340 clones want the vendor driver. Once installed, the port appears as COMn on Windows, /dev/cu.usbserial-… or /dev/cu.SLAB_USBtoUART on macOS, and /dev/ttyUSB0 on Linux.
Boards with native USB need no driver but a different habit. The S2, S3, C3 and C6 can talk USB themselves, showing up as /dev/ttyACM0 on Linux or a "USB JTAG/serial debug unit" on Windows. They may need the BOOT button held while you plug them in to enter download mode, and they re-enumerate after a flash, so the serial monitor may need reconnecting.
Linux permissions. Your user must be in the dialout group (sudo usermod -aG dialout $USER, then log out and in). On Ubuntu, the brltty screen-reader package famously claims CH340 devices the instant they appear; if the port vanishes a second after you plug in, sudo apt remove brltty is the fix.
Something else holds the port. Only one program can open a serial port. Close the Arduino Serial Monitor, a PlatformIO monitor, screen, minicom, or a second browser tab that connected earlier, then try again.
The BOOT button and download mode
An ESP32 decides at reset whether to run its firmware or enter the ROM download mode, and the deciding signal is GPIO0: low at reset means download mode. Every dev board wires the BOOT button to GPIO0 and the EN or RST button to the reset line, and almost every board also connects the USB bridge's DTR and RTS lines to those same pins through a pair of transistors. That circuit is what lets the flasher put the chip into download mode by itself; esptool-js toggles DTR and RTS in the right sequence when you click Connect.
When the auto-reset circuit is missing, weak, or confused by a particular USB bridge, the log shows Connecting.... with a row of dots and then a timeout. The fix is manual:
- Hold BOOT.
- Click Connect and pick the port.
- If it still does not detect, tap EN (or RST) once while BOOT is still held.
- Release BOOT once the log reports the chip.
The flasher shows the detected chip, its MAC address and its flash size before anything is written, so you get a last look at what you are about to program. Native-USB boards are the same story with a twist: hold BOOT while plugging the cable in, then connect.
Baud rate, cables, and the "failed to connect" family
The ROM always negotiates at 115 200 and then switches up to whatever you chose. The flasher defaults to 460 800, which writes a one-megabyte application in roughly twenty-five seconds; 921 600 is faster on a short, good cable; 115 200 is the safe setting that works through long cables and cheap bridges, at the cost of well over a minute per megabyte. Most of the errors you will ever see sort into a short table.
| What the log says | What it means | What to do |
|---|---|---|
Connecting.... then a timeout, or "Failed to connect" |
The chip never entered download mode | Hold BOOT while connecting (previous section) |
| "Timed out waiting for packet header" partway through | The link dropped data at this speed | Lower the baud rate, use a shorter cable, skip the hub |
| "Failed to open serial port" or the port is greyed out | Another program has it | Close the other monitor or tab |
Flash finishes, then the board prints invalid header: 0xffffffff forever |
Nothing bootable at the address the ROM checked | Wrong offset or wrong chip preset; reflash with the table above |
flash read err, 1000 on boot |
The image was built for a flash mode the board cannot use, usually QIO on a DIO board | Rebuild with DIO, or use the board's known-good settings |
| Hash or verify mismatch after writing | Bytes were corrupted on the way | Same as the timeout: slower, shorter, direct |
Anything not in that table is worth pasting into a search with the exact wording, because esptool's messages are stable across versions and the browser build prints the same ones.
Erase, or don't
Ticking Erase entire flash wipes every byte, including the NVS partition where firmware keeps its settings: Wi-Fi credentials, calibration, pairing keys. You want that in two cases: you are moving to a firmware with a different partition layout, or the settings partition is corrupted and the board boot-loops. You do not want it for a routine update. A normal flash writes only the sectors your files cover and leaves NVS alone, which is why re-flashing a Synacl gateway keeps its Wi-Fi and account provisioning and comes straight back online.
Check that it worked
Click Monitor serial after the flash. The first lines of a healthy boot look like rst:0x1 (POWERON_RESET) followed by the bootloader's partition listing and then your application's own output. Two things to watch for: a board that prints the reset line and then repeats it every second is boot-looping, which almost always means a wrong offset or an erase that was needed and skipped; a board that prints nothing at all is usually the monitor at the wrong baud rate, since most firmware logs at 115 200.
Give the board a job
Flashing is the boring half. If the board is a classic ESP32 and you would rather describe what it should do than write firmware for it, the same page has a second mode. Pick Synacl gateway firmware and it fetches the current stable build and writes all four parts at the right offsets in one click, still without an account. The board then waits for Wi-Fi and account credentials, which you give it over the same USB cable from the app: create an account on the free tier, open Gateways → Add gateway, skip the flash step, register the gateway and provision it. The gateway setup guide walks through those three screens, and the console article shows how to watch its boot log from the browser afterwards.
From there the board reads Modbus, I²C, 1-Wire and GPIO devices into dashboards and alerts with no code at all. Two guides pick up where this one stops: ESP32 Modbus RTU to a cloud dashboard in 15 minutes wires an RS485 sensor to the freshly flashed board, and Program an ESP32 without writing code has an AI assistant write the automation that runs on it.
Try it on your own hardware
Synacl is free for five devices — no card, no sales call. Flash a gateway from the browser, add your first device, and see live data in a few minutes.