Skip to content

SSE Event IDs and last-event-id — Tracking Events and Recovering from Disconnections

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about SSE Event IDs and last. We cover key concepts, practical examples, and best practices to help you master this topic.

SSE event IDs use the id: field to assign unique sequence identifiers to events, which the EventSource API uses in the Last-Event-ID header during reconnection to resume the stream from the last successfully received event.

What You'll Learn

  • How to assign IDs to SSE events
  • How the Last-Event-ID header works during reconnection
  • How to implement server-side event buffering for replay

Why It Matters

Without event IDs, a disconnected client misses all events sent during the disconnection. With IDs, the client tells the server the last event it received, and the server replays missed events. This is critical for applications that cannot miss data, like financial tickers or notification systems.

Real-World Use

DodaTech's real-time incident alert system sends SSE events with monotonically increasing IDs. If a security analyst's browser disconnects for 30 seconds, the browser sends Last-Event-ID: 1542 on reconnection, and the server replays events 1543 through 1560, ensuring no alerts are missed.

sequenceDiagram
    participant Client
    participant Server

    Server->>Client: id: 100 | data: {...}
    Server->>Client: id: 101 | data: {...}
    Server->>Client: id: 102 | data: {...}
    Note over Client: Disconnected!
    Note over Client: Reconnecting...
    Client->>Server: Last-Event-ID: 102
    Server->>Client: id: 103 | data: {...}
    Server->>Client: id: 104 | data: {...}

Server-Side Event IDs

import time
import json
from flask import Flask, Response, stream_with_context, request

app = Flask(__name__)

# In-memory event buffer (use Redis in production)
event_buffer = []
event_counter = 0

@app.route('/events/notifications')
def notification_stream():
    last_event_id = request.headers.get('Last-Event-ID')
    def generate():
        global event_counter

        # Replay missed events if Last-Event-ID is provided
        if last_event_id:
            last_id = int(last_event_id)
            missed = [e for e in event_buffer if e['id'] > last_id]
            for event in missed:
                yield f"id: {event['id']}\n"
                yield f"event: {event['type']}\n"
                yield f"data: {json.dumps(event['data'])}\n\n"

        # Send new events
        while True:
            event_counter += 1
            notification = {
                'id': event_counter,
                'type': 'notification',
                'data': {
                    'message': f'Event #{event_counter}',
                    'timestamp': time.time()
                }
            }

            event_buffer.append(notification)
            # Keep only last 1000 events
            if len(event_buffer) > 1000:
                event_buffer.pop(0)

            yield f"id: {notification['id']}\n"
            yield f"event: {notification['type']}\n"
            yield f"data: {json.dumps(notification['data'])}\n\n"

            time.sleep(2)

    return Response(
        stream_with_context(generate()),
        mimetype='text/event-stream'
    )

Client-Side Reconnection

class ResilientEventSource {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.lastEventId = localStorage.getItem('sse_last_event_id');
    this.connect();
  }

  connect() {
    const url = this.lastEventId
      ? `${this.url}?lastEventId=${this.lastEventId}`
      : this.url;

    this.eventSource = new EventSource(url);

    this.eventSource.onopen = () => {
      console.log('SSE connection established');
    };

    this.eventSource.onmessage = (event) => {
      // Store the last event ID
      if (event.lastEventId) {
        this.lastEventId = event.lastEventId;
        localStorage.setItem('sse_last_event_id', event.lastEventId);
      }
      this.handleEvent(event);
    };

    this.eventSource.onerror = (error) => {
      console.warn('SSE connection error, will auto-reconnect');
      // EventSource automatically reconnects
      // The browser sends Last-Event-ID header automatically
    };

    this.eventSource.addEventListener('notification', (event) => {
      const data = JSON.parse(event.data);
      this.showNotification(data);
    });
  }

  handleEvent(event) {
    // Custom event handling
  }

  showNotification(data) {
    const container = document.getElementById('notifications');
    const div = document.createElement('div');
    div.textContent = data.message;
    container.prepend(div);
  }

  close() {
    this.eventSource.close();
  }
}

// Usage
const sse = new ResilientEventSource('/events/notifications');

Server-Side Event Buffer with Redis

import redis
import json
import time

r = redis.Redis(host='localhost', port=6379, db=0)

def store_event(event_id, event_type, data):
    """Store event in Redis for replay"""
    event = {
        'id': event_id,
        'type': event_type,
        'data': data,
        'timestamp': time.time()
    }
    r.xadd('sse:events', event, maxlen=1000)
    return event_id

def get_events_since(last_id):
    """Get all events after a given ID"""
    events = r.xrange('sse:events', min=f'({last_id}', max='+')
    return [
        {'id': e[0].decode(), 'type': e[1][b'type'].decode(), 'data': json.loads(e[1][b'data'])}
        for e in events
    ]

Common Mistakes

1. Not Including id: Field

Without the id: field, the browser cannot send Last-Event-ID during reconnection, and the server cannot replay missed events.

2. Using Non-Unique IDs

Event IDs must be unique and monotonically increasing. Duplicate IDs confuse the reconnection logic.

3. Not Buffering Events on the Server

Without an event buffer, the server cannot replay missed events. Use a circular buffer or Redis stream with a max length.

4. Handling Last-Event-ID on the Wrong Endpoint

The Last-Event-ID header is sent automatically by the browser to the same SSE endpoint. Your event handler must be on the same URL.

5. Ignoring the lastEventId Property

The browser sets event.lastEventId automatically when processing id: fields. Use it client-side for tracking, but the browser handles the header automatically.

Practice Questions

  1. What field assigns an ID to an SSE event?
  2. How does the browser communicate the last received ID on reconnection?
  3. What is needed on the server side to support replay?
  4. How long should you buffer events?
  5. What happens if the server has no events after the given ID?

Answers

  1. The id: field. 2. Via the Last-Event-ID HTTP header in the reconnection request. 3. An event buffer (circular buffer or Redis stream). 4. Long enough to cover typical reconnection delays (e.g., last 1000 events or 5 minutes). 5. The server starts sending new events from the current point.

Challenge

Build a resilient SSE system with: monotonically increasing event IDs, Redis-backed event buffering for replay, proper Last-Event-ID handling on reconnection, and a client that displays missed events when reconnecting.

FAQ

What is the lastEventId property?

A property on the EventSource event object that contains the last received event ID.

How does the browser handle reconnection with Last-Event-ID?

The browser automatically adds a Last-Event-ID header to the reconnection request.

How long should I buffer SSE events?

At least as long as the maximum expected reconnection delay, typically 1000 events or 5 minutes.

What happens if the server cleared the buffer?

The server cannot replay missed events. The client receives events from the current point forward.

Can I use non-numeric event IDs?

Yes, any string is valid as long as it's unique and monotonically increasing.

Mini Project

Build a reliable event notification system with: auto-incrementing event IDs, Redis Stream-based event buffer with configurable retention, proper reconnection handling with Last-Event-ID, and a client-side notification queue that displays missed events after reconnection.

What's Next

  • Learn about retry fields and reconnection timing
  • Explore connection state management with readyState
  • Continue to SSE headers for proper streaming configuration

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro