esp32.diy

ESP32 Web Server Example: Step-by-Step Setup Guide

Oct 15, 2026 · 5 min read · Beginner

ESP32 Web Server Example: Step-by-Step Setup Guide
TL;DR An ESP32 web server lets the board respond to HTTP requests from any browser on the same Wi-Fi network. Using ESPAsyncWebServer in Arduino IDE, you can have a working server running with about a dozen lines of code.
LibraryESPAsyncWebServer + AsyncTCP
Example server port81
Max URI length (WebServer lib)2048 characters
Auto-refresh methodHTML meta http-equiv=refresh tag
Task modelFreeRTOS via dedicated AsyncTCP task
Max query arguments256

What hardware and software do you need?

Before writing a single line of code, get these items ready.

Hardware

Software

WiFi.h ships with the arduino-esp32 core and needs no separate install. ESPAsyncWebServer and AsyncTCP must be added through the Arduino IDE Library Manager or installed manually.

If you prefer the ESP-IDF toolchain instead of Arduino, Espressif's built-in HTTP server component provides httpd_start(), httpd_stop(), and httpd_register_uri_handler() and requires no extra libraries.

How do you write the ESP32 web server code? (Step-by-step)

The sketch below is built directly from the example code in the sources. Replace your_ssid and your_password with your network credentials before uploading.

#include <WiFi.h>
#include "ESPAsyncWebServer.h"

AsyncWebServer server1(81);

const char* ssid     = "your_ssid";
const char* password = "your_password";

void setup() {
  Serial.begin(115200);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
  Serial.println("Connected to the WiFi network");
  Serial.println(WiFi.localIP());

  server1.on("/test_data", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", "Hello from ESP32");
  });

  server1.begin();
}

void loop() {
  // Other application logic can run here
}

What each step does:

  1. Include headers. WiFi.h provides WiFi.begin() and WiFi.localIP(). ESPAsyncWebServer.h brings in the server class and the request/response types.
  2. Create the server object. AsyncWebServer server1(81) creates a server that listens on port 81. Pass a different number to use a different port.
  3. Open Serial. Serial.begin(115200) lets you read status messages and the board's assigned IP address in the Serial Monitor.
  4. Connect to Wi-Fi. WiFi.begin(ssid, password) starts the connection attempt. The while loop polls WiFi.status() every second and blocks until WL_CONNECTED is returned — only then does execution continue.
  5. Print the IP address. WiFi.localIP() returns the address the router assigned to the ESP32. Write it down — you will type it into a browser.
  6. Register a route. server1.on("/test_data", HTTP_GET, handler) maps every GET request arriving at /test_data to a lambda. Inside the lambda, request->send(200, "text/plain", "...") sends an HTTP 200 response with a plain-text body.
  7. Start the server. server1.begin() opens the listening socket and the server begins accepting connections.

How do you verify the web server is working?

  1. Select your ESP32 board and the correct COM port in Arduino IDE, then click Upload.
  2. Open the Serial Monitor (Tools → Serial Monitor) and set the baud rate to 115200.
  3. Wait for "Connected to the WiFi network". The very next line will show an IP address such as 192.168.1.42.
  4. On a device connected to the same Wi-Fi network, open a browser and navigate to:
    http://192.168.1.42:81/test_data
    
    Replace the IP with the one printed in your Serial Monitor.
  5. The browser should display the text your request->send() call returns. If you see it, the server is working correctly.

If the IP address never appears: re-check your SSID and password, and confirm the router is a 2.4 GHz network.

How do you make the page update automatically?

A plain HTTP response does not refresh on its own. The sources describe two approaches.

Option 1 — HTML meta refresh tag (simplest)

Change the handler to return HTML instead of plain text, and include a <meta http-equiv="refresh"> tag in the <head>. The value in content= is the reload interval in seconds.

server1.on("/test_data", HTTP_GET, [](AsyncWebServerRequest *request){
  String html = "<html><head><meta http-equiv=\"refresh\" content=\"30\"></head><body>"
                + String("your data here") + "</body></html>";
  request->send(200, "text/html", html.c_str());
});

Change 30 to whatever interval you want. The browser handles the reload entirely on the client side — the ESP32 simply sends normal HTML each time a request arrives.

Option 2 — Client-side JavaScript

The sources also mention using JavaScript on the client side as an alternative, for example using fetch() to poll an endpoint and update only part of the page without a full reload. This can produce a smoother result for sensor dashboards, though no full JavaScript example is provided in the sources.

Can you run other code in loop() alongside the web server?

Yes. The arduino-esp32 core runs on top of ESP-IDF, which uses FreeRTOS. AsyncTCP runs the server's event callbacks in its own FreeRTOS task, while loop() runs in a separate task. Both take turns on the processor, so you can safely read sensors, update a display, or control GPIOs in loop() without interfering with the web server.

One critical rule: do not call delay() or yield() inside an async server callback. Because these tasks are non-preemptive — only one runs at a time — blocking inside a callback prevents the AsyncTCP event task from processing other events. Pending connections stall and clients time out. If a request handler needs significant processing time, the recommended pattern from the sources is to set a shared flag variable inside the callback and do the actual work in loop() on the next iteration.

What are the most common mistakes?

Mistake What happens Fix
Wrong SSID or password while (WiFi.status() != WL_CONNECTED) never exits; board keeps printing "Connecting to WiFi.." Double-check credentials; confirm the network is 2.4 GHz
Missing port in the browser URL Browser tries port 80; connection is refused Add :81 (or your chosen port) to the URL
Calling delay() inside a callback Blocks the AsyncTCP task; other clients stall or time out Set a shared flag in the callback and do heavy work in loop()
Forgetting server1.begin() Server object is created but never listens Always call begin() at the end of setup()
Path traversal with serveStatic() The hardened WebServer library rejects . or .. path segments and their percent-encoded forms Request canonical paths; do not rely on .. navigation
Relying on a bare Authorization header Older library versions accepted any matching username with no scheme; the hardened library only accepts Basic and Digest Update authentication code to use a proper Basic or Digest scheme

The hardened WebServer library also enforces a default maximum URI length of 2048 characters (WEBSERVER_MAX_URI_LEN) and a per-line read timeout of 5000 ms (WEBSERVER_MAX_LINE_WAIT). Normal browser traffic stays well under both limits, but scripts that send very long URLs or stall mid-request will now receive a clean 414 URI Too Long or 408 Request Timeout response instead of consuming heap memory indefinitely. All these limits are plain macros with #ifndef guards, so they can be overridden at build time without editing the library.

Frequently asked questions

Can I use port 80 instead of port 81?

The port is set by the number passed to the AsyncWebServer constructor. You can use any open port, including 80. The example in the sources uses 81.

Can I put sensor or display code in loop() while the async server is running?

Yes. AsyncTCP runs server callbacks in a dedicated FreeRTOS task, so loop() continues to execute normally. The only restriction is that you must not call delay() or yield() inside the callbacks themselves.

What happens when a request URI exceeds the length limit?

The WebServer library responds with 414 URI Too Long and closes the connection. The default limit is 2048 characters, set by the WEBSERVER_MAX_URI_LEN macro, which can be overridden at build time.

Does the ESP32 support WebSocket servers?

Yes. The ESP-IDF HTTP server component includes WebSocket support, enabled with the CONFIG_HTTPD_WS_SUPPORT option in menuconfig.

How do I serve a page that auto-refreshes with new sensor data?

Return HTML from the handler and include a <meta http-equiv="refresh" content="30"> tag in the <head>, where 30 is the refresh interval in seconds. Set the content type argument in request->send() to "text/html".

Bottom line

Building an ESP32 web server in Arduino IDE is straightforward: connect to Wi-Fi, register route handlers with server.on(), and call server.begin(). ESPAsyncWebServer keeps loop() free for other application tasks while handling HTTP requests in a dedicated FreeRTOS thread. Pay attention to the async callback rules — no delay() inside handlers — and the WebServer library's built-in request limits, and you will have a stable server running on a few dollars of hardware.

Sources

github.comWebServer Request Hardening (arduino-esp32 docs)github.comHTTP Server (esp-idf docs)stackoverflow.comESP32 Web Server periodic updating problemstackoverflow.comWhen setting up a esp32 for an asynchronous web server, can there be code within the void loop()?

This guide summarises the public sources listed above in our own words. Check the original documentation before relying on exact figures.