Skip to content

Auto-Reconnection in SSE — Complete Guide

DodaTech Updated 2026-06-28 6 min read

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

Can the server disable auto-reconnection?

No. The client always attempts to reconnect. To stop reconnection, the client must call source.close().

Does SSE reconnection affect other browser connections?

No. Each EventSource is independent. Reconnection does not affect other SSE streams or HTTP requests.

How do I test SSE reconnection behavior?

Use browser dev tools to throttle the network (Offline mode) or kill the server process. Watch the onopen and onerror events.

What happens to buffered events during reconnection?

They are lost unless the server stores events and replays them based on Last-Event-ID. The browser does not buffer events.

Can I customize reconnection behavior beyond retry?

Not with native EventSource. For full control (custom headers, fetch-based), use a fetch() + ReadableStream polyfill instead.

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