ESP32 Web Server with DHT Sensor

A complete, line‑by‑line explanation of how an ESP32 hosts a live web page that displays real‑time temperature and humidity readings from a DHT sensor.

Overview

This project turns an ESP32 into a small web server. When any device on the same WiFi network opens the ESP32's IP address in a browser, it receives an HTML page that shows the current temperature and humidity read from a DHT11/DHT22 sensor. The page can even refresh itself automatically so the values update without the user reloading the page.

Before diving into every line, it helps to see the "big picture" first — the diagrams below show how the pieces connect before we explain each block of code in detail.

1. Required Libraries

Every ESP32 sketch begins by including the libraries it depends on. For this project we need libraries that handle WiFi connectivity, the web server itself, and communication with the DHT sensor.

#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
LibraryPurpose
WiFi.hConnects the ESP32 to your wireless network.
ESPAsyncWebServer.hCreates a non‑blocking (asynchronous) web server so the server can respond to multiple requests without freezing the sketch.
Adafruit_Sensor.hA common sensor interface required by the DHT library.
DHT.hHandles reading temperature and humidity from DHT11/DHT22 sensors.

2. Network Credentials

Next, the sketch stores your WiFi network name (SSID) and password as constant strings so the ESP32 can connect automatically at startup.

const char* ssid     = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
The ESP32 only supports 2.4 GHz WiFi networks. If your router broadcasts a dual‑band network (2.4 GHz + 5 GHz), make sure the ESP32 connects to the 2.4 GHz band — it cannot see or join a 5 GHz‑only network.

3. Sensor Pin and Type Definition

Here we tell the code which physical GPIO pin the sensor's data line is connected to, and which DHT model is being used (DHT11 or DHT22).

#define DHTPIN 15      // GPIO pin connected to the sensor
#define DHTTYPE DHT22  // Change to DHT11 if that is your sensor model
Wiring Diagram (Conceptual)
DHT22 Sensor
ESP32 GPIO 15
DHT Library reads data

4. Creating the DHT and Server Objects

An object is an instance of a class. Here we create one object for the sensor and one for the web server.

DHT dht(DHTPIN, DHTTYPE);
AsyncWebServer server(80);
CodeExplanation
DHT dht(DHTPIN, DHTTYPE); Creates an instance of the DHT class named dht, configured with the pin and sensor type defined earlier. The name dht is arbitrary — you can rename it.
AsyncWebServer server(80); Creates a web server instance listening on port 80 — the default port for HTTP traffic. If you wanted the site to respond on a custom port instead, you would replace 80 with your chosen port number, e.g. AsyncWebServer server(8080);

5. Functions to Read Temperature and Humidity

Two separate functions read the sensor values and return them as text (String) so they can easily be inserted into the HTML page later. Keeping them as two separate functions (rather than one function handling both) keeps the code simple and avoids the need for pointers.

Read Temperature

String readDHTTemperature() {
  float t = dht.readTemperature();
  if (isnan(t)) {
    Serial.println("Failed to read from DHT sensor!");
    return "--";
  } else {
    Serial.println(t);
    return String(t);
  }
}

Read Humidity

String readDHTHumidity() {
  float h = dht.readHumidity();
  if (isnan(h)) {
    Serial.println("Failed to read from DHT sensor!");
    return "--";
  } else {
    Serial.println(h);
    return String(h);
  }
}
isnan() checks whether the reading is "Not a Number" — meaning the sensor failed to respond correctly. Printing the value to the Serial Monitor is optional; it is only there to help you confirm the readings are correct while debugging.

6. The HTML Page Stored in Flash Memory (PROGMEM)

The web page itself is written as one large HTML string. Because this text can be quite long, storing it in normal RAM would waste valuable memory that the ESP32 needs for other tasks. Instead, the PROGMEM keyword tells the compiler to store the string in flash memory instead of RAM.

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP32 DHT Server</title>
  <meta http-equiv="refresh" content="10">
</head>
<body>
  <h2>ESP32 DHT Sensor Readings</h2>
  <p>Temperature: %TEMPERATURE% &deg;C</p>
  <p>Humidity: %HUMIDITY% %</p>
</body>
</html>
)rawliteral";
Why PROGMEM Matters
Large HTML Text
PROGMEM keyword
Stored in Flash Memory
(saves RAM)
If you prefer to design the page separately, you can copy just the HTML section into a plain text editor, save it with an .html extension, and preview or edit it as a normal webpage before pasting it back into the sketch.

7. The Processor Function — Filling in the Placeholders

Notice the placeholders %TEMPERATURE% and %HUMIDITY% inside the HTML above. The processor function is automatically called by the server and replaces each placeholder with real sensor data before the page is sent to the browser.

String processor(const String& var) {
  if (var == "TEMPERATURE") {
    return readDHTTemperature();
  }
  else if (var == "HUMIDITY") {
    return readDHTHumidity();
  }
  return String();
}
How Placeholder Substitution Works
%TEMPERATURE% found in HTML
processor() called
readDHTTemperature() runs
Real value inserted into page

8. Inside setup()

The setup() function runs once when the ESP32 powers on or resets. Each instruction below executes in sequence, on its own line, in this order:

Serial.begin(115200); // Start serial communication for debugging dht.begin(); // Initialize the DHT sensor WiFi.begin(ssid, password); // Start connecting to WiFi while (WiFi.status() != WL_CONNECTED) { delay(1000); } // Wait until connected Serial.println(WiFi.localIP()); // Print the ESP32's IP address server.on("/", HTTP_GET, handleRoot); // Define what happens on a page request server.begin(); // Start the web server

Handling the Web Request

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", index_html, processor);
});

This tells the server: "When someone requests the root page (/), send them index_html, running every placeholder through the processor function first." As soon as this request arrives, three actions happen almost simultaneously — the page loads, the temperature is read, and the humidity is read — because both placeholders trigger the same processor function automatically.

9. Inside loop()

Unlike many ESP32 sketches, the loop() function here is left almost empty:

void loop() {
  // Intentionally left empty
}

This is because the AsyncWebServer library runs the server in its own background task, independent of loop(). All the real work — accepting connections, calling the processor, sending pages — happens automatically in parallel.

Important: avoid using delay() inside loop() if you add your own code there. A delay() call blocks (pauses) the entire processor, which can interrupt the web server's ability to respond to requests.

10. Full Request Flow Diagram

Putting it all together, here is what happens from the moment a browser requests the page to the moment it displays live sensor data:

Browser requests
ESP32 IP Address
server.on("/") triggered
index_html sent
with processor()
%TEMPERATURE%
→ readDHTTemperature()
%HUMIDITY%
→ readDHTHumidity()
Final HTML page
with real values
Displayed in Browser

Final Tips

TipWhy It Matters
Use a 2.4 GHz WiFi networkESP32 cannot connect to 5 GHz‑only networks.
Keep loop() free of long delaysPrevents blocking the asynchronous web server.
Store large HTML in PROGMEMSaves valuable RAM on the ESP32.
Rename objects freelyNames like dht or server are just labels you choose.
If you are just starting out, don't worry about understanding every single line immediately. Get the project running first — the logic behind each piece will become clearer with practice. Beginner Friendly