Auto-Reconnection in SSE — Complete Guide
In this tutorial, you will learn about Auto. We cover key concepts, practical examples, and best practices to help you master this topic.
Understand SSE auto-reconnection: how browsers reconnect after connection loss, set retry intervals, handle reconnection events, implement exponential backoff, and resume streams gracefully.
What You Learn
You will learn how the EventSource API auto-reconnects, how to set custom retry intervals, how to detect reconnection events, how to implement exponential backoff, and how to resume streams after reconnection.
Why It Matters
Network interruptions are inevitable. SSE's built-in auto-reconnection ensures streams resume without code. Understanding how it works lets you tune retry behavior, prevent thundering herds, and ensure no data is lost.
Real-World Use
DodaTech's dashboard SSE uses a 5-second retry interval with exponential backoff. On connection loss, the server receives Last-Event-ID and replays missed events. Users see no gaps in the data stream.
How Auto-Reconnection Works
// The browser EventSource API reconnects automatically.
// Here is what happens internally:
// 1. Connection drops -> EventSource detects closed socket
// 2. Browser waits (default 2-3 seconds)
// 3. Browser creates new HTTP GET request
// 4. If server sent "id:" fields, browser sends Last-Event-ID header
// 5. New connection established
const source = new EventSource('/events');
source.onopen = () => {
console.log('Connection established (or re-established)');
updateConnectionStatus('connected');
};
source.onerror = (event) => {
if (source.readyState === EventSource.CONNECTING) {
console.log('Connection lost, reconnecting...');
updateConnectionStatus('reconnecting');
} else if (source.readyState === EventSource.CLOSED) {
console.log('Connection closed');
updateConnectionStatus('closed');
}
};
Setting Custom Retry Interval
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import time
class RetrySSE(BaseHTTPRequestHandler):
def do_GET(self):
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()
# Set retry to 10 seconds (10000 ms)
# The browser will wait 10 seconds before reconnecting
self.wfile.write(b"retry: 10000\n\n")
# Send a few events
for i in range(5):
data = json.dumps({'count': i, 'message': f'Event {i}'})
self.wfile.write(f"id: {i + 1}\ndata: {data}\n\n".encode())
time.sleep(1)
# Close the connection to trigger reconnection
# Browser will wait 10 seconds and reconnect
self.wfile.write(b"event: close\ndata: {}\n\n")
// Client side
const source = new EventSource('/events');
source.addEventListener('close', () => {
console.log('Server requested close, will reconnect in ~10s');
});
// The reconnection is automatic.
// The browser now uses the retry: 10000 value.
Exponential Backoff
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import time
import random
class BackoffSSE(BaseHTTPRequestHandler):
def do_GET(self):
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()
# Send initial retry
self.wfile.write(b"retry: 2000\n\n")
# Send events with increasing retry on each disconnect
for i in range(3):
data = json.dumps({'attempt': i + 1})
self.wfile.write(f"id: {i + 1}\ndata: {data}\n\n".encode())
time.sleep(1)
# Server enforces exponential backoff by updating retry
for backoff_time in [5000, 10000, 20000, 30000]:
self.wfile.write(f"retry: {backoff_time}\n\n".encode())
self.wfile.write(
f"event: backoff\ndata: {json.dumps({'retry_ms': backoff_time})}\n\n".encode()
)
time.sleep(2)
// Client-side exponential backoff simulation
class SmartEventSource {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.retryDelay = options.initialRetry || 1000;
this.maxRetry = options.maxRetry || 30000;
this.backoffFactor = options.backoffFactor || 2;
this.connect();
}
connect() {
this.source = new EventSource(this.url);
this.source.onopen = () => {
this.retryDelay = this.options.initialRetry || 1000;
if (this.options.onReconnect) {
this.options.onReconnect();
}
};
this.source.onerror = () => {
this.source.close();
this.retryDelay = Math.min(
this.retryDelay * this.backoffFactor,
this.maxRetry
);
console.log(`Reconnecting in ${this.retryDelay}ms...`);
setTimeout(() => this.connect(), this.retryDelay);
};
if (this.options.onMessage) {
this.source.onmessage = this.options.onMessage;
}
}
close() {
if (this.source) {
this.source.close();
}
}
}
const source = new SmartEventSource('/events', {
initialRetry: 1000,
maxRetry: 30000,
backoffFactor: 2,
onReconnect: () => console.log('Reconnected'),
onMessage: (e) => console.log('Data:', e.data),
});
Reconnection with Last-Event-ID
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import time
class ResumeSSE(BaseHTTPRequestHandler):
event_store = []
counter = 0
def do_GET(self):
last_event_id = self.headers.get('Last-Event-ID')
print(f"Client connecting, last event: {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()
# Replay missed events
if last_event_id:
start_id = int(last_event_id)
for event in ResumeSSE.event_store:
if event['id'] > start_id:
self.wfile.write(f"id: {event['id']}\n".encode())
self.wfile.write(f"event: {event['type']}\n".encode())
self.wfile.write(f"data: {json.dumps(event['data'])}\n\n".encode())
# Continue streaming new events
for i in range(5):
ResumeSSE.counter += 1
event_id = ResumeSSE.counter
data = {'msg': f'Event {event_id}', 'time': time.time()}
ResumeSSE.event_store.append({
'id': event_id,
'type': 'update',
'data': data,
})
self.wfile.write(f"id: {event_id}\n".encode())
self.wfile.write(f"event: update\ndata: {json.dumps(data)}\n\n".encode())
time.sleep(1)
Connection State Management
class ConnectionManager {
constructor(url) {
this.url = url;
this.source = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.listeners = new Map();
}
connect() {
this.source = new EventSource(this.url);
this.source.onopen = () => {
this.reconnectAttempts = 0;
this.emit('status', 'connected');
};
this.source.onerror = () => {
this.emit('status', 'reconnecting');
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.emit('status', 'failed');
this.source.close();
return;
}
this.reconnectAttempts++;
};
this.source.onmessage = (event) => {
this.emit('message', event.data);
};
}
on(event, handler) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(handler);
}
emit(event, data) {
const handlers = this.listeners.get(event) || [];
for (const handler of handlers) {
handler(data);
}
}
close() {
if (this.source) {
this.source.close();
}
}
}
const manager = new ConnectionManager('/events');
manager.on('status', (status) => {
document.getElementById('status').textContent = status;
document.getElementById('status').className = status;
});
manager.on('message', (data) => {
addToLog(JSON.parse(data));
});
manager.connect();
Common Mistakes
1. Relying on Default Retry Only
Default retry (2-3 seconds) is too fast for production. When 100 clients reconnect simultaneously, they create a thundering herd. Set retry to 5-10 seconds.
2. Not Handling Reconnection Events
Users see no feedback during reconnection. Show connection status: connected, reconnecting, or disconnected.
3. No Exponential Backoff
If the server is down, clients reconnect every 3 seconds and never back off. Implement or signal exponential backoff.
4. Not Saving Last-Event-ID
Without tracking event IDs on the server, reconnecting clients miss events sent during disconnection.
5. Server-Side Resource Leaks
Each reconnection creates a new handler. Old handlers that did not detect disconnection accumulate. Use timeouts to detect dead connections.
Practice Questions
1. How does the browser know when to reconnect after SSE disconnection?
The EventSource API watches the readyState. When it transitions to CLOSED or CONNECTING, it waits for the retry interval and creates a new connection.
2. What is the default retry interval in browsers?
2-3 seconds, varying by browser. The server can override this with the retry: field.
3. How does exponential backoff help SSE?
It prevents thundering herd when many clients reconnect simultaneously after a server restart. Each client waits longer between attempts.
4. What information does the browser send on reconnection?
The Last-Event-ID header contains the ID of the last received event. The server uses it to replay missed events.
Challenge
Build an SSE system with: initial retry of 2 seconds, max retry of 60 seconds, backoff factor of 2, Last-Event-ID resumption, connection status indicator on the client, and server-side cleanup of stale connections after 5 minutes of no activity.
FAQ
Mini Project: Reconnection Dashboard
<!DOCTYPE html>
<html>
<head>
<title>SSE Reconnection Demo</title>
<style>
.connected { background: #4caf50; }
.reconnecting { background: #ff9800; }
.disconnected { background: #f44336; }
#status { padding: 10px; color: white; font-weight: bold; }
#log { max-height: 300px; overflow-y: auto; font-family: monospace; }
</style>
</head>
<body>
<div id="status">Connecting...</div>
<div id="log"></div>
<script>
const statusEl = document.getElementById('status');
const logEl = document.getElementById('log');
function log(msg) {
const el = document.createElement('div');
el.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
logEl.prepend(el);
}
function connect() {
const source = new EventSource('/events');
source.onopen = () => {
statusEl.textContent = 'Connected';
statusEl.className = 'connected';
log('Connected');
};
source.onerror = () => {
if (source.readyState === EventSource.CONNECTING) {
statusEl.textContent = 'Reconnecting...';
statusEl.className = 'reconnecting';
log('Reconnecting...');
}
};
source.onmessage = (event) => {
log(`Data: ${event.data}`);
};
window.onbeforeunload = () => source.close();
}
connect();
</script>
</body>
</html>
What's Next
Now that you understand auto-reconnection, explore Last-Event-ID for resuming streams, then learn about SSE headers for proper configuration.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro