Skip to content

EventSource API — Complete Guide

DodaTech Updated 2026-06-28 6 min read

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

Can EventSource send custom headers?

No. The EventSource API does not support custom headers. For auth tokens, use URL parameters or cookies. For full control, use fetch() with ReadableStream.

How many concurrent EventSource connections can a browser open?

HTTP/1.1 limits to 6 per host. HTTP/2 removes this limit. Different subdomains count as separate hosts.

Does EventSource work with HTTPS?

Yes. EventSource works over HTTPS. For local development, most browsers allow HTTP on localhost.

Can I use EventSource in a Service Worker?

No. EventSource is only available in Window contexts. Use fetch() with streaming in Service Workers.

What is the eventPhase property?

eventPhase indicates the state: CONNECTING (0), OPEN (1), or CLOSED (2). It is useful in error handlers to determine if the connection was intentionally closed.

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