ESP32 Based Web Server
The ESP32 Web Server project demonstrates how to build and host a web page directly from an ESP32 microcontroller. This allows users to control devices or monitor sensors from any browser on the same Wi-Fi network.
How It Works
An ESP32 running a web server turns the board itself into the user interface. Rather than printing to a serial console that requires a cable and a computer, the board serves an HTML page that any phone or laptop on the same network can open — no app, no driver, no cable.
The board can operate in two modes. In station mode it joins an existing WiFi network and receives an IP address from the router. In access point mode it creates its own network, which is useful for field devices with no infrastructure. It can also do both at once.
A web server is fundamentally a request router: the client asks for a path such as / or /data, and the sketch decides what to send back. Serving the page and serving the data separately is the pattern that matters — the HTML is static and loads once, while JavaScript on the page polls a lightweight /data endpoint for fresh values.
That separation avoids the beginner mistake of regenerating the entire page on every update, which is slow, flickers, and wastes the ESP32's limited heap. With 520 KB of RAM there is room for a reasonable page, but not for repeatedly concatenating large strings.
Components Needed
- ESP32
- ESP32
- Jumper Wires
- LEDs (optional)
- Power Supply or USB Cable
Wiring to the ESP32
No special wiring is needed beyond power and whatever sensor supplies the data. Connect a sensor to GPIO34 if you want real values to serve rather than a simulated one.
WiFi transmission draws current in short bursts of several hundred milliamps. A weak USB supply or a thin cable causes brownouts that look like random reboots — use a good cable and, ideally, a 500 mA or better supply.
Note the IP address printed to the serial monitor on first boot. Without it you cannot reach the server, though mDNS can give the board a fixed name such as esp32.local instead.
| Requirement | Detail | Notes |
|---|---|---|
| WiFi network | 2.4 GHz only | The ESP32 does not support 5 GHz |
| Sensor input | GPIO34 | Any data source to publish |
| Power | 5 V via USB or 3.3 V regulated | WiFi draws current in bursts |
| Serial monitor | 115200 baud | Needed once to discover the assigned IP |
Build and Upload
Open the Arduino IDE, select the ESP32 board, and paste the provided code. Update the Wi-Fi SSID and password in the sketch before uploading.
Example Code
A web server serving a page once and live values from a separate endpoint. Upload it with the board set to ESP32 and open the Serial Monitor at 115200 baud.
#include <WiFi.h>
#include <WebServer.h>
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASSWORD = "YOUR_PASSWORD";
WebServer server(80);
// Static page — sent once, then JavaScript polls /data for fresh values
const char PAGE[] PROGMEM = R"HTML(
<!DOCTYPE html><html><head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32 Monitor</title>
<style>
body{font-family:system-ui;margin:0;padding:2rem;background:#0f172a;color:#e2e8f0}
.card{background:#1e293b;border-radius:12px;padding:2rem;max-width:420px;margin:auto}
.value{font-size:3rem;font-weight:700;color:#38bdf8}
</style></head><body>
<div class="card">
<h2>Live Reading</h2>
<div class="value" id="v">--</div>
<p id="t">waiting...</p>
</div>
<script>
setInterval(async () => {
const r = await fetch('/data');
const j = await r.json();
document.getElementById('v').textContent = j.value;
document.getElementById('t').textContent = 'uptime ' + j.uptime + ' s';
}, 1000);
</script></body></html>
)HTML";
void handleRoot() { server.send_P(200, "text/html", PAGE); }
void handleData() {
char json[96];
snprintf(json, sizeof(json), "{\"value\":%d,\"uptime\":%lu}",
analogRead(GPIO34), millis() / 1000);
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("connecting");
while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); }
Serial.println();
Serial.print("Open http://");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/data", handleData);
server.begin();
}
void loop() {
server.handleClient();
}
Applications
A esp32 web server turns up in a wide range of projects. These are the uses where it is the right choice rather than a compromise:
- Sensor dashboards readable from any phone on the network
- Configuration interfaces for headless devices
- Home automation control panels
- Remote switching of relays and outputs
- Field instruments in access-point mode with no infrastructure
Working with the ESP32
The ESP32 is built around the ESP32 and runs on 3.3 V logic with 520 KB of SRAM and 4 MB typically of program flash. These details change how this circuit is wired and what the sketch can do, so they are worth stating plainly before you build.
The ESP32 is a 3.3 V part; 5 V sensor outputs need a divider or level shifter before they touch a GPIO.
Its ADC is 12-bit, so readings span 0–4095 rather than the 0–1023 an Arduino returns.
ADC2 pins stop working once WiFi is active — keep analog sensors on ADC1 (GPIO32–GPIO39) in any connected project.
GPIO34–GPIO39 are input-only and have no internal pull-ups.
| ESP32 characteristic | Value | Why it matters here |
|---|---|---|
| Logic voltage | 3.3 V | Sensor outputs above this level need a divider or level shifter |
| ADC resolution | 12-bit (0–4095) | Sets how finely an analog reading can be resolved |
| Analog inputs | ADC1 on GPIO32–GPIO39 and ADC2 on several other pins | Determines how many analog sensors can share the board |
| PWM outputs | any GPIO through the LEDC peripheral | Needed for brightness, speed and tone control |
| I²C pins | GPIO21 (SDA) and GPIO22 (SCL) by default | Fixed by hardware — wiring copied from another board may not match |
| Interrupt pins | any GPIO | Required for counting fast or asynchronous events |
| Serial | three hardware UARTs | Monitor runs at 115200 baud by default |
Troubleshooting
Most problems with this module fall into a handful of categories. Work through these before suspecting the part itself:
- It never connects — the network is 5 GHz. The ESP32 supports 2.4 GHz only.
- The board reboots repeatedly — the supply cannot deliver WiFi current bursts. Use a better cable and supply.
- The page loads but values never update — the
/dataendpoint is failing; open it directly in the browser to see the error. - The IP address changes between boots — the router is assigning dynamically. Reserve an address, or use mDNS.
- Memory runs out after a while — the page HTML is being built with String concatenation. Keep it in PROGMEM as shown.
- The sketch compiles but the board resets or behaves erratically — a 5 V module output is being driven into a 3.3 V pin. Measure the signal before connecting it.
- Readings differ from an Arduino tutorial for the same part — the 12-bit ADC returns 0–4095, not 0–1023, so any constant copied from an Uno example needs rescaling.
Taking It Further on the ESP32
Once the basic reading works, where you go next depends very much on which board you are using. These are the directions that suit the ESP32 specifically:
With WiFi and Bluetooth built in, the natural extension is to publish readings over MQTT or expose them as a BLE characteristic that a phone app can subscribe to directly.
The ESP32’s dual cores let acquisition and networking run independently. Pin a FreeRTOS task reading the sensor to one core and the WiFi stack to the other, and network delays stop disturbing sample timing.
Deep sleep with the RTC timer brings average current into the microamp range, and the ULP co-processor can keep watching an input while the main cores stay asleep — practical for battery sensors that must last months.
Notes and Practical Limits
Serving the page from PROGMEM rather than building it as a String is the single most important detail here. String concatenation fragments the heap and eventually crashes a long-running server.
For anything beyond a handful of clients, the asynchronous ESPAsyncWebServer library handles concurrent connections far better than the blocking WebServer, which processes one request at a time.