EventSource API — Complete Guide
In this tutorial, you will learn about EventSource API. We cover key concepts, practical examples, and best practices to help you master this topic.
Use the EventSource API to consume SSE streams: create connections, handle events, manage reconnection, set last-event-id, close connections, and handle errors gracefully.
What You Learn
You will learn how to use the browser EventSource API, handle different event types, implement reconnection logic, work with last-event-id for resuming streams, and best practices for consuming SSE in web applications.
Why It Matters
The EventSource API is the client-side interface for SSE. Understanding its features enables reliable real-time updates. Misusing it causes memory leaks, unnecessary reconnections, and lost events.
Real-World Use
DodaTech's monitoring dashboard creates an EventSource for each dashboard widget. Each connection handles its own event types, reconnects independently, and closes when the widget is removed from the view.
Basic EventSource Usage
// Create a connection
const source = new EventSource('/api/events');
// Listen for all messages
source.onmessage = function(event) {
console.log('Received:', event.data);
updateUI(JSON.parse(event.data));
};
// Connection opened
source.onopen = function(event) {
console.log('SSE connection established');
showConnectionStatus('connected');
};
// Error handling
source.onerror = function(event) {
if (event.eventPhase === EventSource.CLOSED) {
console.log('Connection closed by server');
showConnectionStatus('disconnected');
} else {
console.log('Connection error (will auto-reconnect)');
showConnectionStatus('reconnecting');
}
};
// Close connection (when done)
function cleanup() {
source.close();
console.log('SSE connection closed');
}
Named Events
const source = new EventSource('/api/stream');
source.addEventListener('user-login', (event) => {
const data = JSON.parse(event.data);
console.log(`${data.user} logged in at ${data.time}`);
showUserOnline(data.user);
});
source.addEventListener('notification', (event) => {
const notif = JSON.parse(event.data);
showNotification(notif.title, notif.body);
updateBadge(notif.count);
});
source.addEventListener('heartbeat', (event) => {
// Keep-alive, no UI update needed
lastHeartbeat = Date.now();
});
source.addEventListener('error', (event) => {
const error = JSON.parse(event.data);
showErrorAlert(error.message);
});
// Catch unnamed events
source.onmessage = (event) => {
console.log('Default handler:', event.data);
};
Reconnection and Last-Event-ID
// The browser handles reconnection automatically.
// When reconnecting, it sends the Last-Event-ID header.
// The server can use this to resume the stream.
class ReliableEventSource {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.lastId = null;
this.connect();
}
connect() {
const url = this.lastId
? `${this.url}?lastId=${this.lastId}`
: this.url;
this.source = new EventSource(url);
this.source.onmessage = (event) => {
// Track the last event ID for reconnection
if (event.lastEventId) {
this.lastId = event.lastEventId;
}
if (this.options.onMessage) {
this.options.onMessage(event);
}
};
this.source.onerror = () => {
console.log('Connection lost, will auto-reconnect');
// The browser will reconnect automatically.
// The Last-Event-ID header will be sent if available.
};
this.source.onopen = () => {
console.log('Connected (or reconnected)');
if (this.options.onReconnect && this.lastId) {
this.options.onReconnect(this.lastId);
}
};
}
close() {
if (this.source) {
this.source.close();
}
}
}
const events = new ReliableEventSource('/api/events', {
onMessage: (event) => updateUI(JSON.parse(event.data)),
onReconnect: (lastId) => console.log(`Resumed from ${lastId}`),
});
Connection States
const source = new EventSource('/api/events');
// EventSource.readyState values:
// 0 = CONNECTING
// 1 = OPEN
// 2 = CLOSED
function checkConnection() {
switch (source.readyState) {
case EventSource.CONNECTING:
return 'Connecting';
case EventSource.OPEN:
return 'Connected';
case EventSource.CLOSED:
return 'Closed';
}
}
// Monitor connection state
setInterval(() => {
const state = checkConnection();
document.getElementById('status').textContent = state;
if (state === 'Closed' && !userInitiatedClose) {
// Unexpected close, reconnect manually if needed
// (though EventSource usually auto-reconnects)
}
}, 1000);
Managing Multiple EventSources
class EventSourceManager {
constructor() {
this.sources = new Map();
}
add(name, url, handlers = {}) {
if (this.sources.has(name)) {
console.warn(`Source ${name} already exists, replacing`);
this.remove(name);
}
const source = new EventSource(url);
this.sources.set(name, source);
// Default handler
if (handlers.onmessage) {
source.onmessage = handlers.onmessage;
}
// Named event handlers
if (handlers.events) {
for (const [event, handler] of Object.entries(handlers.events)) {
source.addEventListener(event, handler);
}
}
// Error handler
if (handlers.onerror) {
source.onerror = handlers.onerror;
}
return source;
}
remove(name) {
const source = this.sources.get(name);
if (source) {
source.close();
this.sources.delete(name);
}
}
removeAll() {
for (const [name, source] of this.sources) {
source.close();
}
this.sources.clear();
}
getCount() {
return this.sources.size;
}
}
const manager = new EventSourceManager();
manager.add('notifications', '/api/notifications', {
events: {
'alert': (e) => showAlert(JSON.parse(e.data)),
'info': (e) => showInfo(JSON.parse(e.data)),
},
onerror: (e) => console.error('Notification stream error'),
});
manager.add('metrics', '/api/metrics', {
onmessage: (e) => updateChart(JSON.parse(e.data)),
});
Server-Side Stream Generation
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import time
class ServerSideEvents(BaseHTTPRequestHandler):
def do_GET(self):
last_id = self.headers.get('Last-Event-ID')
self.send_response(200)
self.send_header('Content-Type', 'text/event-stream')
self.send_header('Cache-Control', 'no-cache')
self.send_header('Connection', 'keep-alive')
self.end_headers()
event_id = int(last_id) if last_id else 0
start = time.time()
while time.time() - start < 30:
event_id += 1
data = json.dumps({
'id': event_id,
'time': time.time(),
'value': event_id * 10,
})
self.wfile.write(f"id: {event_id}\n".encode())
self.wfile.write(f"data: {data}\n\n".encode())
time.sleep(1)
Common Mistakes
1. Not Handling Errors
Without onerror handler, connection errors go unnoticed. Always handle errors and show connection status to users.
2. Not Closing Connections
Leaving EventSource connections open when leaving a page wastes resources. Close connections in cleanup or component unmount.
3. Ignoring lastEventId
The event object has a lastEventId property. Use it for custom reconnection logic or logging.
4. Multiple Sources Without Management
Creating multiple EventSources without tracking them makes cleanup impossible. Use a manager pattern.
5. Assuming EventSource Works in All Browsers
IE does not support EventSource. Use a Polyfill or fall back to polling for IE users.
Practice Questions
1. How does the browser handle SSE reconnection?
Automatically. When the connection drops, the browser waits (default 2-3 seconds) and creates a new EventSource with the Last-Event-ID header if available.
2. How do you close an EventSource connection?
Call source.close(). This sets readyState to CLOSED and prevents further reconnection attempts.
3. What is the lastEventId property?
The event object's lastEventId contains the ID of the last received event. The browser sends this as Last-Event-ID on reconnection.
4. How do you listen for specific event types?
Use source.addEventListener('eventname', handler) where eventname matches the "event:" field sent by the server.
Challenge
Build a dashboard that opens 3 SSE streams (notifications, metrics, logs), shows connection status for each, handles reconnection gracefully, closes streams when navigating away, and uses a polyfill for browsers without EventSource support.
FAQ
Mini Project: EventSource Dashboard
<!DOCTYPE html>
<html>
<head>
<title>SSE Dashboard</title>
<style>
.connected { color: green; }
.disconnected { color: red; }
#events { max-height: 400px; overflow-y: auto; }
</style>
</head>
<body>
<h1>SSE Live Dashboard</h1>
<p id="status" class="disconnected">Disconnected</p>
<div id="events"></div>
<script>
const eventsDiv = document.getElementById('events');
const statusEl = document.getElementById('status');
function addEvent(type, data) {
const el = document.createElement('div');
el.innerHTML = `<strong>${type}:</strong> ${JSON.stringify(data)}`;
eventsDiv.prepend(el);
}
const source = new EventSource('/api/stream');
source.onopen = () => {
statusEl.textContent = 'Connected';
statusEl.className = 'connected';
};
source.onerror = () => {
statusEl.textContent = 'Disconnected (reconnecting...)';
statusEl.className = 'disconnected';
};
source.addEventListener('metrics', (e) => addEvent('metrics', JSON.parse(e.data)));
source.addEventListener('alert', (e) => addEvent('alert', JSON.parse(e.data)));
source.onmessage = (e) => addEvent('message', JSON.parse(e.data));
// Close on page unload
window.addEventListener('beforeunload', () => source.close());
</script>
</body>
</html>
What's Next
Now that you understand the EventSource API, learn SSE with Express.js, then explore SSE with Django.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro