If you want to build a stable ESP32 webserver for a smart home sensor node, use the built-in synchronous WebServer.h library paired with an ESP32-WROOM-32 DevKit V1 (30-pin). While asynchronous libraries exist, the synchronous approach handles under 5 concurrent clients flawlessly, avoids dependency hell, and compiles out-of-the-box on the Arduino framework. Below is the exact hardware spec sheet, pin mapping, and production-ready code to get environmental data and relay control running on your local network.
The Verdict: Which ESP32 Web Server Library Should You Use?
Before writing a single line of code, you must choose your server architecture. Picking the wrong library is the leading cause of watchdog resets and memory leaks in embedded projects.
| Library | Concurrency | Memory Footprint | Best Use Case |
|---|---|---|---|
WebServer.h (Sync) |
1 client at a time | Low (~20KB RAM) | Simple dashboards, single-user config portals |
ESPAsyncWebServer |
Multiple concurrent | High (~45KB+ RAM) | WebSockets, Server-Sent Events (SSE), multi-user apps |
MicroPython socket |
Manual handling | Variable | Rapid prototyping, non-Arduino ecosystems |
WebServer.h. It is baked into the ESP32 Arduino core, requires zero external library manager installations, and eliminates the version-conflict errors common with third-party async forks. Only upgrade to ESPAsyncWebServer if you need real-time graph updates via WebSockets.
Hardware Spec Sheet & Pin Mapping
This build targets the ESP32-WROOM-32 DevKit V1 (30-pin variant). Do not use the 38-pin variant for this specific wiring diagram, as the GPIO numbering shifts on the wider boards.
Parts List
- MCU: ESP32-WROOM-32 DevKit V1 (30-pin, Type-C or Micro-USB)
- Sensor: BME280 I2C Temperature/Humidity/Pressure breakout (Adafruit or generic)
- Actuator: SRD-05VDC-SL-C 5V Relay Module (Must have optocoupler isolation)
- Power: 5V 2A USB power supply (Do not use a PC USB port; they limit current to 500mA)
Pin Mapping Table
| Component | Component Pin | ESP32 GPIO | Notes |
|---|---|---|---|
| BME280 | VIN / VCC | 3V3 | Do not use 5V; the BME280 is strictly 3.3V. |
| BME280 | GND | GND | Common ground required. |
| BME280 | SDA | GPIO 21 | Default I2C SDA on 30-pin DevKit. |
| BME280 | SCL | GPIO 22 | Default I2C SCL on 30-pin DevKit. |
| Relay Module | VCC | 5V (VIN) | Powered from ESP32 VIN pin (USB 5V rail). |
| Relay Module | GND | GND | Common ground required. |
| Relay Module | IN | GPIO 25 | Active LOW trigger. 3.3V logic compatible. |
Step-by-Step Build & Compilable Code
Before uploading, install the Adafruit BME280 Library and Adafruit Unified Sensor library via the Arduino Library Manager. The code below includes robust WiFi reconnection logic and I2C initialization error handling.
- Wire the BME280 to the I2C pins (21/22) and the Relay IN pin to GPIO 25.
- Remove the JD-VCC jumper on the relay module if present, ensuring the optocoupler is powered separately from the coil (prevents back-EMF resets).
- Copy the code below into your Arduino IDE. Update the
ssidandpasswordvariables. - Upload at 115200 baud. Open the Serial Monitor to retrieve the assigned IP address.
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
// --- NETWORK CREDENTIALS ---
const char* ssid = "YOUR_2.4GHZ_SSID";
const char* password = "YOUR_PASSWORD";
// --- PIN DEFINITIONS ---
#define RELAY_PIN 25
// --- OBJECTS ---
WebServer server(80);
Adafruit_BME280 bme;
// --- STATE ---
bool relayState = false;
unsigned long lastReconnectAttempt = 0;
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // Active LOW: HIGH = OFF
// Initialize I2C Sensor with error handling
if (!bme.begin(0x76)) { // Try 0x76 first, some breakouts use 0x77
if (!bme.begin(0x77)) {
Serial.println("[ERROR] Could not find a valid BME280 sensor, check wiring!");
while (1); // Halt execution
}
}
// Connect to WiFi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nConnected! IP: " + WiFi.localIP().toString());
} else {
Serial.println("\n[ERROR] WiFi connection failed. Rebooting...");
ESP.restart();
}
// --- ROUTES ---
server.on("/", HTTP_GET, handleRoot);
server.on("/toggle", HTTP_GET, handleToggle);
server.onNotFound([]() {
server.send(404, "text/plain", "404: Endpoint not found");
});
server.begin();
}
void loop() {
// WiFi watchdog reconnect
if (WiFi.status() != WL_CONNECTED) {
if (millis() - lastReconnectAttempt > 5000) {
lastReconnectAttempt = millis();
Serial.println("[WARN] WiFi disconnected. Reconnecting...");
WiFi.reconnect();
}
}
server.handleClient();
}
void handleRoot() {
float temp = bme.readTemperature();
float hum = bme.readHumidity();
String html = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<style>body{font-family:sans-serif;text-align:center;padding:20px;} .btn{padding:15px 30px;font-size:18px;}</style></head><body>";
html += "<h2>ESP32 Sensor Dashboard</h2>";
html += "<p>Temperature: " + String(temp) + " °C</p>";
html += "<p>Humidity: " + String(hum) + " %</p>";
html += "<p>Relay State: " + String(relayState ? "ON" : "OFF") + "</p>";
html += "<a href='/toggle'><button class='btn'>Toggle Relay</button></a>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleToggle() {
relayState = !relayState;
digitalWrite(RELAY_PIN, relayState ? LOW : HIGH); // Active LOW
Serial.println("Relay toggled to: " + String(relayState));
server.sendHeader("Location", "/");
server.send(303);
}
Debugging: Exact Errors & The First 3 Checks
When your ESP32 webserver crashes or fails to connect, the serial monitor tells you exactly what went wrong—if you know how to read it. Here are the most common failure modes.
Ranked Causes for "Brownout detector was triggered"
Exact Error String: Brownout detector was triggered followed by a continuous reboot loop.
- USB Cable Voltage Drop (Most Likely): Cheap USB cables use 28AWG wire, causing a massive voltage drop when the relay coil engages. The ESP32's 3.3V LDO starves, triggering the hardware brownout reset. Fix: Use a high-quality 20AWG data cable or a dedicated 5V 2A wall adapter.
- Relay Back-EMF: If the relay module lacks proper optocoupler isolation or a flyback diode, the inductive kickback from the coil collapses the 5V rail. Fix: Ensure the JD-VCC jumper on the relay module is removed, and power the coil side from a separate 5V source if using high loads.
- AMS1117 LDO Overheating: The onboard voltage regulator on generic DevKits maxes out around 800mA. If you are powering external 5V peripherals directly from the ESP32's VIN pin while drawing heavy current, it will thermally throttle and drop voltage.
Ranked Causes for WiFi Connection Failures
Exact Error String: wl_status: 6 (WL_DISCONNECTED) or wl_status: 1 (WL_NO_SSID_AVAIL)
- 5GHz Network Band: The ESP32-WROOM-32 only supports 802.11 b/g/n on the 2.4GHz band. It physically cannot see 5GHz or 6GHz SSIDs. Fix: Connect to a dedicated 2.4GHz IoT VLAN or SSID.
- WPA3 Security Protocol: Older ESP32 Arduino cores (pre-2.0.0) struggle with WPA3-Personal handshakes. Fix: Update your ESP32 Board Manager package to the latest 2.x or 3.x release, or set your router to WPA2/WPA3 transition mode.
- Cable Gauge & Power: Swap the USB cable. 80% of ESP32 instability is traced back to inadequate power delivery over thin wires.
- I2C Address Conflict: Run an I2C scanner sketch. BME280 breakouts default to either
0x76or0x77depending on the manufacturer. If your code hardcodes the wrong address, the sensor hangs the bus. - Router Isolation: Ensure your router's "AP Isolation" or "Client Isolation" is disabled, otherwise your phone cannot route HTTP requests to the ESP32's local IP address.
Extending and Simplifying the Build
Once the baseline dashboard is stable, you can scale the project up or strip it down based on your deployment environment.
How to Simplify (The Bare-Metal Toggle)
If you don't need environmental data and just want a web-controlled smart plug, strip out the Wire.h and Adafruit_BME280 libraries. Delete the bme.begin() block and the sensor reading variables. This frees up roughly 15KB of flash memory and eliminates I2C bus lockups, making the firmware virtually bulletproof for simple GPIO toggling.
How to Extend (mDNS and OTA)
For a production deployment where you don't want to memorize IP addresses or plug in a USB cable to update firmware:
- Add mDNS: Include
#include <ESPmDNS.h>and callMDNS.begin("esp32-relay")in setup. You can now access the server viahttp://esp32-relay.localon any modern browser. - Add ArduinoOTA: Include the
ArduinoOTA.hlibrary to enable wireless firmware updates over your local network. This is critical once the ESP32 is mounted inside an enclosure or behind a wall switch.
For deeper insights into ESP32 WiFi driver behaviors and power management, refer to the Espressif WiFi Driver API Guide. For sensor calibration and I2C wiring specifics, consult the Adafruit BME280 Breakout Documentation.






