ESP32 Web Server Tutorial: Build a Live Sensor Dashboard with Arduino IDE (2026 Guide)
If you can blink an LED and connect to WiFi, the next skill worth learning is turning your ESP32 into its own web server — no phone app, no cloud account, no third-party broker. Just a browser and your board's IP address.
Most tutorials stop at toggling two LEDs from a webpage. That's a fine first step, but it teaches a pattern that breaks the moment you add a sensor: using the blocking WiFiServer/client.println() approach, your ESP32 freezes mid-request if anything else is happening on the chip, and you'll eventually hit a watchdog reset. This guide skips straight to the library that's actually production-ready — ESPAsyncWebServer — and builds a live-updating sensor dashboard the right way.
Quick answer
Use the ESPAsyncWebServer library with AsyncTCP, not the classic WiFi.h + WebServer.h combo. It handles multiple clients without blocking your main loop, supports JSON responses cleanly, and won't trigger Task watchdog got triggered crashes when a sensor read takes a few milliseconds longer than expected.
What you'll need
| Component | Notes |
|---|---|
| ESP32 dev board | Any variant — DevKit V1, ESP32-S3, ESP32-C6 all work |
| DHT22 or BME280 sensor | Optional, for the live-data example |
| USB cable | Data-capable, not charge-only |
| Arduino IDE | With ESP32 board package installed |
Step 1: Install the libraries
In Arduino IDE, go to Sketch → Include Library → Manage Libraries and install:
ESPAsyncWebServer(by lacamera or me-no-dev fork, both maintained)AsyncTCP
If you've already followed xloge's ESP32 WiFi connection guide, your WiFi credentials block is already sorted — reuse it here.
Step 2: Build the base server
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected. IP: " + WiFi.localIP().toString());
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/html", "<h1>ESP32 Dashboard</h1>");
});
server.begin();
}
void loop() {
// Nothing needed here — the async server runs independently
}Notice loop() stays empty. That's the whole point: the async server handles requests on its own, so loop() is free for sensor polling, MQTT publishing, or FreeRTOS tasks without contention.
Step 3: Serve live sensor data as JSON
Rather than baking HTML into String concatenations (fragile and memory-hungry), serve raw data and let the browser render it:
#include <DHT.h>
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
server.on("/data", HTTP_GET, [](AsyncWebServerRequest *request) {
float temp = dht.readTemperature();
float hum = dht.readHumidity();
String json = "{\"temp\":" + String(temp) + ",\"hum\":" + String(hum) + "}";
request->send(200, "application/json", json);
});Then in your root page's HTML, a small JavaScript fetch('/data') call on a setInterval refreshes the numbers every few seconds — no full page reload, no flicker.
Step 4: Serve the dashboard from LittleFS instead of hardcoded strings
For anything beyond a demo, store your HTML/CSS/JS as real files:
#include <LittleFS.h>
void setup() {
LittleFS.begin();
server.serveStatic("/", LittleFS, "/www/").setDefaultFile("index.html");
// ...rest of setup
}Upload files via the ESP32 Filesystem Uploader plugin. This keeps your sketch readable and your webpage editable without recompiling firmware.
Common problems and mistakes
- "Guru Meditation Error" / watchdog reset — almost always from using the blocking
WiFiServerpattern with a slow operation (sensor read, SD card write) inside the request handler. Switching toESPAsyncWebServerfixes this by design. - Browser shows stale data — add
Cache-Control: no-storeheaders to/dataresponses, or your phone browser will cache the JSON. - Page works on WiFi but not from outside your network — that's expected; this is a local web server. For remote access you'd need port forwarding or a reverse tunnel, which is a separate (and riskier) topic.
- Multiple browser tabs cause disconnects — this only happens with the old synchronous
WebServer.h, which handles one client at a time. Async server handles concurrent clients natively.
Web server vs. MQTT: which should you use?
Since xloge already covers MQTT with Home Assistant, it's worth being direct about when each fits:
| Use a web server when… | Use MQTT when… |
|---|---|
| You want a self-contained device with no broker | You're integrating into Home Assistant or a larger IoT stack |
| One or two devices, checked manually | Many devices, need centralized logging/automation |
| You want zero external dependencies | You want push-based, low-latency updates |
They're not mutually exclusive — many real builds run both: MQTT for automation, a local web page for manual override and diagnostics.
Advanced insight: securing your dashboard
If the ESP32 is reachable by anyone on the network (shared WiFi, office, dorm), add basic authentication:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
if (!request->authenticate("admin", "your_password")) {
return request->requestAuthentication();
}
request->send(200, "text/html", "<h1>Secured Dashboard</h1>");
});This isn't encryption — traffic is still plain HTTP on your LAN — but it stops casual snooping. True TLS on ESP32 is possible but resource-heavy and usually unnecessary for a local dashboard.
FAQ
Do I need a DHT22 sensor for this tutorial to work?
No — Steps 1–2 give you a working web server with no sensor at all. The DHT22 is only for the live-data example in Step 3.
Can I use this with ESP32-S3 or ESP32-C6 instead of the original ESP32?
Yes, ESPAsyncWebServer and AsyncTCP work across the ESP32 family with no code changes.
Why does my page load but never update the temperature?
Check the browser console — it's almost always a mismatched fetch URL or the JSON handler not registered before server.begin().
Is ESPAsyncWebServer better than WebServer.h for beginners too, or just advanced projects?
Better for both. It's not meaningfully harder to write, and it avoids a class of crashes beginners usually can't diagnose.
Can this web server run at the same time as MQTT publishing?
Yes — that's exactly why the async approach matters. loop() stays free to handle MQTT (or FreeRTOS tasks, as in xloge's FreeRTOS guide) while the server handles requests in the background.
How do I access the dashboard from outside my home network?
Not covered here by design — it requires port forwarding or a tunneling service and introduces real security exposure. Treat it as a separate, deliberate decision.
Final takeaway
The two-LED demo you'll find everywhere teaches the wrong default. Start with ESPAsyncWebServer, serve JSON instead of hardcoded HTML strings, and keep your files on LittleFS — and you'll have a pattern that scales from "blink an LED" to a full sensor dashboard without hitting a rewrite wall.
