The Direct Answer: What is an ESP32 Web Config Portal?
An ESP32 web config portal is a captive network interface that allows end-users to input WiFi credentials and custom device parameters (like MQTT broker IPs or API keys) via a smartphone or laptop browser, without hardcoding them into the firmware. When the ESP32 boots and cannot find a known network—or when a physical config button is pressed—it spins up an Access Point (AP), intercepts DNS requests to force a browser popup, and serves a configuration form. Once submitted, the credentials are saved to non-volatile storage (NVS) and the device reboots into Station (STA) mode.
This eliminates the need to recompile firmware for every deployment and prevents the security risk of shipping hardcoded network passwords in your source code.
Provisioning Methods Compared
Before wiring up the board, you need to choose the right provisioning architecture. Here is how the standard ESP32 web config approach stacks up against Espressif's native alternatives for a 2026 deployment.
| Method | Blocking/Non-Blocking | Custom Params | Flash Wear | Best For |
|---|---|---|---|---|
| WiFiManager (Captive Portal) | Blocking (during setup) | Yes (HTML forms) | Low (if using NVS) | Consumer IoT, DIY smart home |
| ESPAsyncWebServer (Custom UI) | Non-Blocking | Yes (Full SPA/React) | Medium (LittleFS) | Complex dashboards, industrial |
| SmartConfig (Espressif App) | Blocking | No (WiFi only) | Low | Mass production, no UI needed |
| BLE Provisioning | Non-Blocking | Yes (via BLE GATT) | Low | High-security enterprise IoT |
Source: Espressif Arduino Core Documentation and tzapu WiFiManager Repository.
Hardware & Pin Mapping
We are targeting the ESP32-WROOM-32E (DevKitC V4, 38-pin variant). The 'E' variant features an updated 4MB flash layout and improved RF matching over the obsolete V1 silicon. Avoid the original 30-pin DevKit V1 boards for new designs; their strapping pin layouts cause chronic boot-loop issues when wiring buttons.
Parts List
- MCU: ESP32-WROOM-32E DevKitC V4 (38-pin, USB-C)
- Switch: 6x6mm tactile momentary pushbutton
- Resistor: 10kΩ (optional, only if your board lacks an internal pull-up on GPIO 0)
- LED: 5mm through-hole LED with 220Ω current-limiting resistor
Pin Mapping Table
| ESP32 GPIO | Component | Function | Notes / Constraints |
|---|---|---|---|
| GPIO 0 | Tactile Switch | Config Trigger (BOOT) | Must be HIGH at boot. Switch pulls to GND. |
| GPIO 2 | LED + 220Ω Resistor | Status Indicator | Also a strapping pin; safe to use as output post-boot. |
| GND | Switch & LED Cathode | Common Ground | Ensure solid breadboard contact to avoid floating inputs. |
The Code: Non-Blocking Config with NVS
This implementation uses the WiFiManager library (v2.0.17+) alongside the ESP32's native Preferences.h (NVS). We avoid LittleFS/SPIFFS here because NVS requires no custom partition table modifications, saving you from the most common flash-wear and boot-partition errors.
Required Libraries (via Arduino Library Manager):
WiFiManagerby tablatronix (v2.0.17 or newer)ArduinoJsonby Benoit Blanchon (v6.21+ or v7) - Included in this sketch for future extensibility, though NVS handles the raw strings here.
#include <WiFi.h>
#include <WiFiManager.h>
#include <Preferences.h>
// --- PIN DEFINITIONS ---
#define CONFIG_PIN 0 // BOOT button on DevKitC V4
#define STATUS_LED 2 // Built-in or external LED
// --- GLOBALS ---
Preferences prefs;
char mqtt_server[40] = "192.168.1.100"; // Default fallback
bool shouldSaveConfig = false;
// --- CALLBACK: Triggered when user hits 'Save' in the web portal ---
void saveConfigCallback () {
shouldSaveConfig = true;
}
void setup() {
Serial.begin(115200);
delay(100); // Allow serial buffer to settle
pinMode(CONFIG_PIN, INPUT_PULLUP);
pinMode(STATUS_LED, OUTPUT);
digitalWrite(STATUS_LED, LOW);
Serial.println("\n--- ESP32 Web Config Boot ---");
// 1. Initialize NVS (Non-Volatile Storage)
prefs.begin("iot-config", false);
prefs.getString("mqtt_ip", mqtt_server, 40);
Serial.print("Loaded MQTT Server: ");
Serial.println(mqtt_server);
// 2. Setup WiFiManager
WiFiManager wm;
// Add custom parameter for the web config portal
WiFiManagerParameter custom_mqtt("mqtt_ip", "MQTT Broker IP", mqtt_server, 40);
wm.addParameter(&custom_mqtt);
// Bind the save callback
wm.setSaveConfigCallback(saveConfigCallback);
// Set portal timeout (seconds) to prevent infinite AP mode
wm.setConfigPortalTimeout(180);
// 3. Trigger Logic: AutoConnect vs Forced Config Portal
if (digitalRead(CONFIG_PIN) == LOW) {
Serial.println("Config button pressed. Forcing Web Config Portal...");
digitalWrite(STATUS_LED, HIGH); // Solid ON = Config Mode
// Blocks here until user configures or timeout occurs
if (!wm.startConfigPortal("ESP32-WebConfig", "setup1234")) {
Serial.println("[ERROR] Config portal timeout or failure. Restarting.");
ESP.restart();
}
} else {
Serial.println("Attempting AutoConnect...");
digitalWrite(STATUS_LED, HIGH);
delay(100);
digitalWrite(STATUS_LED, LOW); // Flash to show activity
if (!wm.autoConnect("ESP32-WebConfig", "setup1234")) {
Serial.println("[ERROR] AutoConnect failed. Restarting.");
ESP.restart();
}
}
// 4. Save Custom Parameters if the form was submitted
if (shouldSaveConfig) {
strncpy(mqtt_server, custom_mqtt.getValue(), 40);
prefs.putString("mqtt_ip", mqtt_server);
Serial.println("Configuration saved to NVS.");
shouldSaveConfig = false;
}
// 5. Connected State
Serial.print("Connected! IP: ");
Serial.println(WiFi.localIP());
// Blink LED to indicate successful connection
for(int i=0; i<5; i++) {
digitalWrite(STATUS_LED, HIGH); delay(100);
digitalWrite(STATUS_LED, LOW); delay(100);
}
}
void loop() {
// Your main application logic (MQTT, sensor reads) goes here.
// Avoid using delay() in the main loop; use millis() for timing.
}
Debugging: Exact Errors and the "First Three" Checklist
When an ESP32 web config portal fails, it usually happens at the intersection of RF physics, DNS hijacking, and flash memory allocation. Before rewriting your code, run through this diagnostic sequence.
The "First Three" Things to Check
- Strapping Pin Conflicts: If the board boots into the serial bootloader (you see
waiting for downloadin the Serial Monitor), GPIO 0 is being held LOW. Check your tactile switch wiring and ensure no external peripherals are pulling GPIO 0, GPIO 2, or GPIO 12 down during the 50ms boot window. - Captive Portal DNS Interception: Modern smartphones (especially iOS 16+ and Android 14+) aggressively block captive portal DNS hijacking if the AP doesn't serve a valid HTTPS certificate. If the popup doesn't appear, manually open a browser and navigate to
http://192.168.4.1(the default ESP32 AP IP). - NVS Namespace Corruption: If the ESP32 continuously reboots when loading credentials, the NVS partition may be corrupted from a previous firmware flash that used a different partition scheme. Use the Arduino IDE "Erase All Flash Before Upload" option once to clear the slate.
Exact Error Strings & Ranked Causes
[WiFiManager] AutoConnect: FAILED
- Cause A (80%): You are trying to connect to a 5GHz WiFi network. The ESP32-WROOM-32E is strictly a 2.4GHz (802.11 b/g/n) radio. It cannot physically see 5GHz SSIDs.
- Cause B (15%): Router MAC filtering or WPA3-Enterprise incompatibility. The ESP32 Arduino core supports WPA2-Personal natively; WPA3 requires specific core configurations.
- Fix: Ensure your router broadcasts a 2.4GHz band with WPA2-PSK (AES) security.
Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1)
- Cause (95%): You placed a blocking function (like
delay(1000), a synchronousHTTPClientcall, or a bit-banged I2C read) inside thesaveConfigCallbackor the WiFiManager event loop. - Fix: The callback must only set a boolean flag (as done in the code above:
shouldSaveConfig = true;). Handle all NVS writes and network calls in the mainsetup()flow after the portal closes.
E (1234) nvs: nvs_open_from_partition failed: NOT_FOUND
- Cause (90%): You selected a partition scheme in the Arduino IDE Tools menu that lacks an NVS partition (e.g., "Huge APP (3MB No OTA/1MB SPIFFS)" sometimes misconfigures NVS boundaries on 4MB boards).
- Fix: Set Tools > Partition Scheme to "Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)". This guarantees the standard NVS partition is intact.
Extending and Simplifying the Build
Depending on your production timeline, you may need to scale this architecture up or down.
How to Simplify (WiFi Only)
If you do not need custom parameters like MQTT IPs or API keys, strip out Preferences.h and WiFiManagerParameter entirely. WiFiManager automatically handles WiFi SSID and Password storage in the background using the ESP32's native WiFi class persistence. Your setup reduces to three lines:
WiFiManager wm;
wm.autoConnect("ESP32-Setup");
// Proceed with standard WiFi.localIP() logic
How to Extend (Full Web Dashboard)
For commercial products requiring a post-configuration dashboard (e.g., viewing real-time sensor graphs or updating OTA firmware via a browser), the captive portal isn't enough.
1. Switch your storage from NVS to LittleFS to host static HTML/CSS/JS files.
2. Implement ESPAsyncWebServer and AsyncTCP to serve those files non-blocking.
3. Use the WiFiManager portal strictly for the initial WiFi handshake, then redirect the user's browser to the ESP32's local mDNS address (e.g., http://mydevice.local) where the AsyncWebServer takes over.
For deeper reading on asynchronous server implementation, refer to the ArduinoJson documentation for parsing REST API payloads sent from your custom web dashboard back to the ESP32.






