This tutorial shows how to use Server-Sent Events (SSE) in an ESP32 Web Server programmed with Arduino IDE. SSE allows the browser to receive automatic updates from a server via HTTP connection. This is useful to send updated sensor readings to the browser, for example. Whenever a new reading is available, the ESP32 sends it to the client and the web page can be updated automatically without the need to make additional requests.

As an example, we’ll build a web server that displays sensor readings from a BME280 temperature, humidity and pressure sensor. To learn more about the BME280, read our guide:
We also have a similar Server-Sent Events guide for the ESP8266.
Server-Sent Events (SSE) allows the client to receive automatic updates from a server via HTTP connection.

The client initiates the SSE connection and the server uses the event source protocol to send updates to the client. The client will receive updates from the server, but it can’t send any data to the server after the initial handshake.
This is useful to send updated sensor readings to the browser. Whenever a new reading is available, the ESP32 sends it to the client and the web page can be updated automatically without the need for further requests. Instead of sensor readings, you can send any data that might be useful for your project like GPIO states, notifications when motion is detected, etc.
Important: Server-Sent Events (SSE) are not supported on Internet Explorer.
Here’s the web page we’ll build for this project.

The following diagram summarizes how Server-Sent Events work to update the web page.

We’ll program the ESP32 board using Arduino IDE, so make sure you have it installed in your Arduino IDE.
To build the web server we’ll use the ESPAsyncWebServer (by ESP32Async) library. This library needs the AsyncTCP (by ESP32Async).
You can install these libraries in the Arduino Library Manager. Open the Library Manager by clicking the Library icon at the left sidebar.
Search for ESPAsyncWebServer and install the ESPAsyncWebServer by ESP32Async.

Then, install the AsyncTCP library. Search for AsyncTCP and install the AsyncTCP by ESP32Async.

To get readings from the BME280 sensor module, we’ll use the Adafruit_BME280 library. You also need to install the Adafruit_Sensor library.
You can also install those libraries via the Arduino IDE Library Manager.
To learn more about the BME280 sensor, read our guide: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity).
To exemplify how to use server-sent events with the ESP32, we’ll send sensor readings from a BME280 sensor to the browser. So, you need to wire a BME280 sensor to your ESP32.
To complete this tutorial you need the following parts:
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
We’re going to use I2C communication with the BME280 sensor module. For that, wire the sensor to the default ESP32 SCL (GPIO 22) and SDA (GPIO 21) pins, as shown in the following schematic diagram.

Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
Copy the following code to your Arduino IDE.
/********* Rui Santos & Sara Santos - Random Nerd Tutorials Complete project details at https://RandomNerdTutorials.com/esp32-web-server-sent-events-sse/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. *********/ #include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> #include <Adafruit_BME280.h> #include <Adafruit_Sensor.h> // Replace with your network credentials const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Create AsyncWebServer object on port 80 AsyncWebServer server(80); // Create an Event Source on /events AsyncEventSource events("/events"); // Timer variables unsigned long lastTime = 0; unsigned long timerDelay = 30000; // Create a sensor object Adafruit_BME280 bme; // BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL) float temperature; float humidity; float pressure; // Init BME280 void initBME(){ if (!bme.begin(0x76)) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); } } void getSensorReadings(){ temperature = bme.readTemperature(); // Convert temperature to Fahrenheit //temperature = 1.8 * bme.readTemperature() + 32; humidity = bme.readHumidity(); pressure = bme.readPressure()/ 100.0F; } // Initialize WiFi void initWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi .."); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); } String processor(const String& var){ getSensorReadings(); //Serial.println(var); if(var == "TEMPERATURE"){ return String(temperature); } else if(var == "HUMIDITY"){ return String(humidity); } else if(var == "PRESSURE"){ return String(pressure); } return String(); } const char index_html[] PROGMEM = R"rawliteral( <!DOCTYPE HTML><html> <head> <title>ESP Web Server</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous"> <link rel="icon" href="data:,"> <style> html {font-family: Arial; display: inline-block; text-align: center;} p { font-size: 1.2rem;} body { margin: 0;} .topnav { overflow: hidden; background-color: #50B8B4; color: white; font-size: 1rem; } .content { padding: 20px; } .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); } .cards { max-width: 800px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .reading { font-size: 1.4rem; } </style> </head> <body> <div class="topnav"> <h1>BME280 WEB SERVER (SSE)</h1> </div> <div class="content"> <div class="cards"> <div class="card"> <p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p><p><span class="reading"><span id="temp">%TEMPERATURE%</span> °C</span></p> </div> <div class="card"> <p><i class="fas fa-tint" style="color:#00add6;"></i> HUMIDITY</p><p><span class="reading"><span id="hum">%HUMIDITY%</span> %</span></p> </div> <div class="card"> <p><i class="fas fa-angle-double-down" style="color:#e1e437;"></i> PRESSURE</p><p><span class="reading"><span id="pres">%PRESSURE%</span> hPa</span></p> </div> </div> </div> <script> if (!!window.EventSource) { var source = new EventSource('/events'); source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false); source.addEventListener('temperature', function(e) { console.log("temperature", e.data); document.getElementById("temp").innerHTML = e.data; }, false); source.addEventListener('humidity', function(e) { console.log("humidity", e.data); document.getElementById("hum").innerHTML = e.data; }, false); source.addEventListener('pressure', function(e) { console.log("pressure", e.data); document.getElementById("pres").innerHTML = e.data; }, false); } </script> </body> </html>)rawliteral"; void setup() { Serial.begin(115200); initWiFi(); initBME(); // Handle Web Server server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/html", index_html, processor); }); // Handle Web Server Events events.onConnect([](AsyncEventSourceClient *client){ if(client->lastId()){ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); } // send event with message "hello!", id current millis // and set reconnect delay to 1 second client->send("hello!", NULL, millis(), 10000); }); server.addHandler(&events); server.begin(); } void loop() { if ((millis() - lastTime) > timerDelay) { getSensorReadings(); Serial.printf("Temperature = %.2f ºC \n", temperature); Serial.printf("Humidity = %.2f \n", humidity); Serial.printf("Pressure = %.2f hPa \n", pressure); Serial.println(); // Send Events to the Web Client with the Sensor Readings events.send("ping",NULL,millis()); events.send(String(temperature).c_str(),"temperature",millis()); events.send(String(humidity).c_str(),"humidity",millis()); events.send(String(pressure).c_str(),"pressure",millis()); lastTime = millis(); } }
Insert your network credentials in the following variables and the code will work straight away.
const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Continue reading to learn how the code works or skip to the Demonstration section.
The Adafruit_Sensor and Adafruit_BME280 libraries are needed to interface with the BME280 sensor.
#include <Adafruit_BME280.h> #include <Adafruit_Sensor.h>
The WiFi, ESPAsyncWebServer and AsyncTCP libraries are used to create the web server.
#include <WiFi.h> #include "ESPAsyncWebServer.h"
Insert your network credentials in the following variables, so that the ESP32 can connect to your local network using Wi-Fi.
const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Create an AsyncWebServer object on port 80.
AsyncWebServer server(80);
The following line creates a new event source on /events.
AsyncEventSource events("/events");
The lastTime and the timerDelay variables will be used to update sensor readings every X number of seconds. As an example, we’ll get new sensor readings every 30 seconds (30000 milliseconds). You can change that delay time in the timerDelay variable.
unsigned long lastTime = 0; unsigned long timerDelay = 30000;
Create an Adafruit_BME280 object called bme on the default ESP32 I2C pins.
Adafruit_BME280 bme;The temperature, humidity and pressure float variables will be used to hold BME280 sensor readings.
float temperature; float humidity; float pressure;
The following function can be called to initialize the BME280 sensor.
void initBME(){ if (!bme.begin(0x76)) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); } }
The getSensorReading() function gets temperature, humidity and pressure readings from the BME280 sensor and saves them on the temperature, humidity and pressure variables.
void getSensorReadings(){ temperature = bme.readTemperature(); // Convert temperature to Fahrenheit //temperature = 1.8 * bme.readTemperature() + 32; humidity = bme.readHumidity(); pressure = bme.readPressure()/ 100.0F; }
The following function sets the ESP32 as a Wi-Fi station and connects to your router using the ssid and password defined earlier.
void initWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi .."); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); }
The processor() function replaces any placeholders on the HTML text used to build the web page with the current sensor readings before sending it to the browser.
String processor(const String& var){ getSensorReadings(); //Serial.println(var); if(var == "TEMPERATURE"){ return String(temperature); } else if(var == "HUMIDITY"){ return String(humidity); } else if(var == "PRESSURE"){ return String(pressure); } }
This allows us to display the current sensor readings on the web page when you access it for the first time. Otherwise, you would see a blank space until new readings were available (which can take some time depending on the delay time you’ve defined on the code).
The index_html variable contains all the HTML, CSS and JavaScript to build the web page.
Note: for the simplicity of this tutorial, we’re placing everything needed to build the web page on the index_html variable that we use on the Arduino sketch. Note that it may be more practical to have separated HTML, CSS and JavaScript files that then you upload to the ESP32 filesystem and reference them on the code.
Here’s the content index_html variable:
<!DOCTYPE HTML> <html> <head> <title>ESP Web Server</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="icon" href="data:,"> <style> html { font-family: Arial; display: inline-block; text-align: center; } p { font-size: 1.2rem; } body { margin: 0; } .topnav { overflow: hidden; background-color: #50B8B4; color: white; font-size: 1rem; } .content { padding: 20px; } .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); } .cards { max-width: 800px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .reading { font-size: 1.4rem; } </style> </head> <body> <div class="topnav"> <h1>BME280 WEB SERVER (SSE)</h1> </div> <div class="content"> <div class="cards"> <div class="card"> <p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p> <p><span class="reading"><span id="temp">%TEMPERATURE%</span> °C</span></p> </div> <div class="card"> <p><i class="fas fa-tint" style="color:#00add6;"></i> HUMIDITY</p> <p><span class="reading"><span id="hum">%HUMIDITY%</span> %</span></p> </div> <div class="card"> <p><i class="fas fa-angle-double-down" style="color:#e1e437;"></i> PRESSURE</p><p><span class="reading"><span id="pres">%PRESSURE%</span> hPa</span></p> </div> </div> </div> <script> if (!!window.EventSource) { var source = new EventSource('/events'); source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false); source.addEventListener('temperature', function(e) { console.log("temperature", e.data); document.getElementById("temp").innerHTML = e.data; }, false); source.addEventListener('humidity', function(e) { console.log("humidity", e.data); document.getElementById("hum").innerHTML = e.data; }, false); source.addEventListener('pressure', function(e) { console.log("pressure", e.data); document.getElementById("pres").innerHTML = e.data; }, false); } </script> </body> </html>
We won’t go into detail on how the HTML and CSS works. We’ll just take a look at how to handle the events sent by the server.
Between the <style> </style> tags we include the styles to style the web page using CSS. Feel free to change it to make the web page look as you wish. We won’t explain how the CSS for this web page works because it is not relevant for this tutorial.
<style> html { font-family: Arial; display: inline-block; text-align: center; } p { font-size: 1.2rem; } body { margin: 0; } .topnav { overflow: hidden; background-color: #50B8B4; color: white; font-size: 1rem; } .content { padding: 20px; } .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); } .cards { max-width: 800px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .reading { font-size: 1.4rem; } </style>
Between the <body> </body> tags we add the web page content that is visible to the user.
<body> <div class="topnav"> <h1>BME280 WEB SERVER (SSE)</h1> </div> <div class="content"> <div class="cards"> <div class="card"> <p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p> <p><span class="reading"><span id="temp">%TEMPERATURE%</span> °C</span></p> </div> <div class="card"> <p><i class="fas fa-tint" style="color:#00add6;"></i> HUMIDITY</p> <p><span class="reading"><span id="hum">%HUMIDITY%</span> %</span></p> </div> <div class="card"> <p><i class="fas fa-angle-double-down" style="color:#e1e437;"></i> PRESSURE</p><p><span class="reading"><span id="pres">%PRESSURE%</span> hPa</span></p> </div> </div> </div>
There’s a heading 1 with the content “BME280 WEB SERVER (SSE)”. That’s the text that shows up on the top bar. Feel free to modify that text.
<h1>BME280 WEB SERVER (SSE)</h1>
Then, we display the sensor readings in separated div tags. Let’s take a quick look at the paragraphs that displays the temperature:
<p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p> <p><span class="reading"><span id="temp">%TEMPERATURE%</span> °C</span></p>
The first paragraph simply displays the “TEMPERATURE” text. We define the color and also an icon.
On the second paragraph, you can see that the %TEMPERATURE% placeholder is surrounded by <span id=”temp”> </span> tags. The HTML id attribute is used to specify a unique id for an HTML element.
It is used to point to a specific style or it can be used by JavaScript to access and manipulate the element with that specific id. That’s what we’re going to do. For instance, when the client receives a new event with the latest temperature reading, it updates the HTML element with the id “temp” with the new reading.
A similar process is done to update the other readings.
The JavaScript goes between the <script> </script> tags. It is responsible for initializing an EventSource connection with the server and handle the events received from the server.
<script> if (!!window.EventSource) { var source = new EventSource('/events'); source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false); source.addEventListener('temperature', function(e) { console.log("temperature", e.data); document.getElementById("temp").innerHTML = e.data; }, false); source.addEventListener('humidity', function(e) { console.log("humidity", e.data); document.getElementById("hum").innerHTML = e.data; }, false); source.addEventListener('pressure', function(e) { console.log("pressure", e.data); document.getElementById("pres").innerHTML = e.data; }, false); } </script>
Let’s take a look at how this works.
Create a new EventSource object and specify the URL of the page sending the updates. In our case, it’s /events.
if (!!window.EventSource) { var source = new EventSource('/events');
Once you’ve instantiated an event source, you can start listening for messages from the server with addEventListener().
These are the default event listeners, as shown here in the AsyncWebServer documentation.
source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false);
Then, add the event listener for “temperature”.
source.addEventListener('temperature', function(e) {
When a new temperature reading is available, the ESP32 sends an event (“temperature”) to the client. The following lines handle what happens when the browser receives that event.
console.log("temperature", e.data); document.getElementById("temp").innerHTML = e.data;
Basically, print the new readings on the browser console, and put the received data into the element with the corresponding id (“temp“) on the web page.
A similar processor is used for humidity and pressure.
source.addEventListener('humidity', function(e) { console.log("humidity", e.data); document.getElementById("hum").innerHTML = e.data; }, false); source.addEventListener('pressure', function(e) { console.log("pressure", e.data); document.getElementById("pres").innerHTML = e.data; }, false); source.addEventListener('gas', function(e) { console.log("gas", e.data); document.getElementById("gas").innerHTML = e.data; }, false);
In the setup(), initialize the Serial Monitor, initialize Wi-Fi and the BME280 sensor.
Serial.begin(115200); initWiFi(); initBME();
When you access the ESP32 IP address on the root / URL, send the text that is stored on the index_html variable to build the web page and pass the processor as argument, so that all placeholders are replaced with the latest sensor readings.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/html", index_html, processor); });
Set up the event source on the server.
// Handle Web Server Events events.onConnect([](AsyncEventSourceClient *client){ if(client->lastId()){ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); } // send event with message "hello!", id current millis // and set reconnect delay to 1 second client->send("hello!", NULL, millis(), 10000); }); server.addHandler(&events);
Finally, start the server.
server.begin();
In the loop(), get new sensor readings:
getSensorReadings();
Print the new readings in the Serial Monitor.
Serial.printf("Temperature = %.2f ºC \n", temperature); Serial.printf("Humidity = %.2f % \n", humidity); Serial.printf("Pressure = %.2f hPa \n", pressure); Serial.println();
Finally, send events to the browser with the newest sensor readings to update the web page.
// Send Events to the Web Server with the Sensor Readings events.send("ping",NULL,millis()); events.send(String(temperature).c_str(),"temperature",millis()); events.send(String(humidity).c_str(),"humidity",millis()); events.send(String(pressure).c_str(),"pressure",millis());
After inserting your network credentials on the ssid and password variables, you can upload the code to your board. Don’t forget to check if you have the right board and COM port selected.
After uploading the code, open the Serial Monitor at a baud rate of 115200 and press the on-board EN/RST button. The ESP IP address should be printed.

Open a browser on your local network and insert the ESP32 IP address. You should get access to the web page to monitor the sensor readings.

The readings are updated automatically every 30 seconds.
At the same time, you should get new sensor readings on the Serial Monitor as shown in the previous print screen. Additionally, you can check if the client is receiving the events. On your browser, open the console by pressing Ctrl+Shift+J.

Copyright ©2025. All Rights Reserved Emblab THE RAVE INNOVATION