
Stop Polling: Build Real-Time Apps with Server-Sent Events
Need real-time updates for your web app? Skip the complexity of WebSockets and the inefficiency of polling. Discover the power of Server-Sent Events (SSE).
We’ve all been there. Your product manager walks over and says, "We need the dashboard to update in real-time when a new order comes in." Your immediate thought is probably one of two things: setting up a messy setInterval to constantly ask the server for updates, or diving headfirst into the complex world of WebSockets.
But what if I told you there is a third, built-in browser standard that is perfectly designed for this exact scenario? It requires no heavy external libraries, scales beautifully over standard HTTP, and has automatic reconnection built right into the browser.
Welcome to the unsung hero of real-time web development: Server-Sent Events (SSE).
The Problems with Polling and WebSockets
Before we look at the solution, let’s quickly review why our default instincts often lead to architectural pain.
HTTP Polling: The Anti-Pattern
Polling is the simplest approach. You write a setInterval loop in the frontend that sends an HTTP request to your REST API every 5 seconds.
While trivial to implement, this is incredibly inefficient. If no new orders come in for an hour, your client has still sent 720 useless HTTP requests, consuming server resources, draining the user's laptop battery, and clogging up the network. You are essentially implementing a DDoS attack against your own backend.
WebSockets: The Overkill Solution
WebSockets are amazing. They provide a persistent, full-duplex, bidirectional communication channel over a single TCP connection. If you are building a real-time multiplayer game or a collaborative editor, WebSockets are the right tool for the job.
However, WebSockets come with significant operational baggage:
- Statefulness: Your load balancers need to be configured for long-lived TCP connections and sticky sessions.
- Infrastructure: Standard HTTP routing doesn't apply. You often need dedicated WebSocket servers.
- Reconnections: The native WebSocket browser API does not handle reconnections automatically. You have to write custom backoff logic or rely on heavy libraries like Socket.io.
The reality? 90% of "real-time" features in standard web applications are purely unidirectional. The server needs to push a notification or a progress bar tick to the client. The client doesn't need to stream data back. For these cases, WebSockets are massive overkill.
Enter Server-Sent Events (SSE)

Server-Sent Events allow a web server to push real-time updates to a browser over a standard, single HTTP connection.
Under the hood, SSE is just an HTTP request where the server responds with a Content-Type of text/event-stream and keeps the connection open. The server then writes formatted text chunks to this stream whenever it has new data.
Here is why SSE is often the optimal choice for unidirectional real-time data:
- It is just HTTP: Works seamlessly with your existing load balancers and reverse proxies (like Nginx) with minimal configuration.
- Built-in Reconnection: The
EventSourceAPI in the browser automatically handles network drops and reconnects natively. - Event IDs: If a connection drops, the browser automatically sends the ID of the last message it received when reconnecting, allowing your server to replay missed events.
- Lightweight: No heavy npm packages are required.
Building a Live Feed with SSE

Let’s see how ridiculously simple this is to implement using Node.js, Express, and standard TypeScript.
The Backend (Express.js)
To turn a standard Express endpoint into an SSE stream, we just need to set specific headers and write data in a specific text format (data: your payload\n\n).
import express, { Request, Response } from 'express';
const app = express();
app.get('/api/live-updates', (req: Request, res: Response) => {
// 1. Set the headers to establish an SSE connection
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Send an initial connection message
res.write('data: {"status": "connected"}\n\n');
// 2. Simulate server pushing data asynchronously
const intervalId = setInterval(() => {
const payload = JSON.stringify({
time: new Date().toISOString(),
activeUsers: Math.floor(Math.random() * 1000)
});
// You can specify custom event names!
res.write('event: metricsUpdate\n');
res.write(`data: ${payload}\n\n`);
}, 3000);
// 3. Crucial: Clean up when the client closes the tab
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(3000, () => console.log('SSE Server running on port 3000'));
Notice the cleanup step. If you forget req.on('close'), your server will leak memory and continue firing intervals for users who have already closed their browsers.
The Frontend (Vanilla JS)
On the client side, we use the native EventSource interface. No installations required.
// 1. Open the connection
const eventSource = new EventSource('/api/live-updates');
// 2. Listen for generic messages
eventSource.onmessage = (event) => {
console.log('Generic data received:', event.data);
};
// 3. Listen for our custom 'metricsUpdate' event
eventSource.addEventListener('metricsUpdate', (event) => {
const data = JSON.parse(event.data);
console.log('Live Active Users:', data.activeUsers);
});
// 4. Handle errors (Browser auto-reconnects!)
eventSource.onerror = (error) => {
console.error('SSE Error, reconnecting...', error);
};
// To manually stop the stream (e.g., component unmounts):
// eventSource.close();
In a React useEffect or Vue onMounted hook, you simply instantiate the EventSource and make sure to call eventSource.close() in your cleanup function to prevent ghost connections.
The Limitations
While SSE is fantastic, you should be aware of its boundaries:
- Unidirectional only: If the client needs to frequently stream data to the server (like a chat app), use WebSockets. You can pair SSE with standard POST requests for occasional actions, but for high-frequency bi-directional data, WebSockets win.
- HTTP/1.1 Connection Limits: Browsers limit the number of simultaneous HTTP/1.1 connections to the same domain (typically 6). If a user opens 7 tabs of your dashboard, the 7th tab will hang. However, if your server uses HTTP/2, this limit is eliminated through multiplexing.
- Binary Data: SSE is designed for UTF-8 text. If you need to stream raw binary data, WebSockets are much more efficient.
Conclusion
The next time you are tasked with building a live dashboard, a notification bell, or an order tracking map, stop and ask yourself: Does the client actually need to talk back, or is it just listening?
If it's just listening, put down the heavy WebSocket libraries. Step away from the setInterval polling. Embrace Server-Sent Events. It's a standard, reliable, and incredibly simple tool that deserves a prominent spot in your software engineering toolbelt. Keep your systems clean, and let the browser do the heavy lifting.
written by
Nguyên Tech
Responses
Loading comments…