Skip to content

Sse Last Event Id

DodaTech 6 min read

title: "SSE Last-Event-ID" description: "Learn how the Last-Event-ID header enables state recovery in Server-Sent Events, allowing clients to resume interrupted event streams." weight: 20 date: 2026-06-28 lastmod: 2026-06-28 tags: ["apis", "sse"]


The Last-Event-ID mechanism allows SSE clients to resume event streams from where they left off after a disconnection. This enables reliable event delivery and state recovery without data loss.

## What You'll Learn

- How Last-Event-ID works
- Sending event IDs from the server
- Reading Last-Event-ID header on reconnect
- Implementing event replay/recovery
- Managing ID state on the client

## Why It Matters

Without Last-Event-ID, clients lose events that occurred during disconnection. For applications that need reliable event delivery, implementing ID-based recovery is essential.

## Real-World Use

A financial trading platform sends event IDs with every market data update. If a trader's connection drops, the server replays missed events when they reconnect, ensuring no trade signals are lost.

## Flow Chart

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server
    
    C->>S: GET /events
    S-->>C: id: 1  data: ...
    S-->>C: id: 2  data: ...
    S-->>C: id: 3  data: ...
    Note over C,S: Connection Lost
    C->>C: Records lastEventId = 3
    Note over C,S: Reconnection
    C->>S: GET /events
    C->>S: Last-Event-ID: 3
    S-->>C: id: 3  data: ... (replayed)
    S-->>C: id: 4  data: ...

Code Examples

Example 1: Server with Event ID Replay

const http = require('http');

// Event store
const eventStore = [];
const MAX_EVENTS = 5000;
let globalId = 0;

// Generate events periodically
function generateEvent() {
  globalId++;
  const event = {
    id: `evt-${globalId}`,
    data: {
      id: globalId,
      type: 'update',
      value: Math.random(),
      timestamp: Date.now(),
    },
  };
  eventStore.push(event);
  if (eventStore.length > MAX_EVENTS) {
    eventStore.shift();
  }
}

setInterval(generateEvent, 500);

http.createServer((req, res) => {
  if (req.url !== '/events') {
    res.writeHead(404);
    res.end();
    return;
  }

  // Read Last-Event-ID from header
  const lastEventId = req.headers['last-event-id'];
  const lastSeq = lastEventId 
    ? parseInt(lastEventId.replace('evt-', '')) 
    : 0;

  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
  });

  // Replay missed events
  const missedEvents = eventStore.filter(e => {
    const eventSeq = parseInt(e.id.replace('evt-', ''));
    return eventSeq > lastSeq;
  });

  if (missedEvents.length > 0) {
    console.log(`Replaying ${missedEvents.length} missed events`);
    missedEvents.forEach(event => {
      res.write(`id: ${event.id}\ndata: ${JSON.stringify(event.data)}\n\n`);
    });
  }

  // Subscribe to new events
  const listener = (event) => {
    res.write(`id: ${event.id}\ndata: ${JSON.stringify(event.data)}\n\n`);
  };

  // In production, use an event emitter instead
  const interval = setInterval(() => {
    const recentEvents = eventStore.filter(e => e.id.startsWith('evt-'));
    if (recentEvents.length > 0) {
      const lastEvent = recentEvents[recentEvents.length - 1];
      const eventSeq = parseInt(lastEvent.id.replace('evt-', ''));
      if (eventSeq > lastSeq) {
        lastSeq = eventSeq;
        res.write(`id: ${lastEvent.id}\ndata: ${JSON.stringify(lastEvent.data)}\n\n`);
      }
    }
  }, 500);

  req.on('close', () => {
    clearInterval(interval);
  });
}).listen(3000);

Expected output: Server replays missed events on reconnection based on the Last-Event-ID header.

Example 2: Client-Side ID Tracking

class ReliableEventSource {
  constructor(url) {
    this.url = url;
    this.lastEventId = localStorage.getItem('sse-last-id') || null;
    this.listeners = new Map();
    this.connect();
  }

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

    this.source = new EventSource(url);

    this.source.onopen = () => {
      console.log('Connected, last ID:', this.lastEventId);
      this.emit('connected');
    };

    this.source.onmessage = (event) => {
      // Track last event ID
      if (event.lastEventId) {
        this.lastEventId = event.lastEventId;
        localStorage.setItem('sse-last-id', event.lastEventId);
      }
      this.emit('message', JSON.parse(event.data));
    };

    this.source.onerror = () => {
      // EventSource will auto-reconnect with Last-Event-ID header
      this.emit('reconnecting');
    };
  }

  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event).add(callback);
    return () => this.listeners.get(event)?.delete(callback);
  }

  emit(event, data) {
    this.listeners.get(event)?.forEach(cb => cb(data));
  }

  clearRecovery() {
    this.lastEventId = null;
    localStorage.removeItem('sse-last-id');
  }

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

// Usage with persistent recovery
const sse = new ReliableEventSource('/events');

sse.on('message', (data) => {
  updateDashboard(data);
});

sse.on('connected', () => {
  updateStatus('connected');
});

sse.on('reconnecting', () => {
  updateStatus('reconnecting');
});

// Manual recovery reset
document.getElementById('refresh-btn').onclick = () => {
  sse.clearRecovery();
  sse.close();
  window.location.reload();
};

Expected output: Client persists last event ID in localStorage, enabling recovery across page reloads.

Example 3: Ordered Event Delivery with IDs

// Server with ordered event delivery
const http = require('http');
const { EventEmitter } = require('events');

class OrderedSSEServer {
  constructor() {
    this.emitter = new EventEmitter();
    this.sequenceMap = new Map(); // connection -> lastSeq
    this.globalSeq = 0;
  }

  handleConnection(req, res) {
    const lastEventId = parseInt(
      req.headers['last-event-id']?.replace('seq-', '')
    ) || 0;

    const connId = `${Date.now()}-${Math.random()}`;
    this.sequenceMap.set(connId, lastEventId);

    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
    });

    // Send missed events from buffer
    const buffer = this.getEventBuffer(lastEventId);
    buffer.forEach(entry => {
      res.write(
        `id: seq-${entry.seq}\n` +
        `event: ${entry.event}\n` +
        `data: ${JSON.stringify(entry.data)}\n` +
        '\n'
      );
    });

    // Subscribe to new events with ordering
    const listener = (eventData) => {
      const currentSeq = this.sequenceMap.get(connId) || 0;
      const nextSeq = currentSeq + 1;

      const message = 
        `id: seq-${nextSeq}\n` +
        `event: ${eventData.event}\n` +
        `data: ${JSON.stringify(eventData.data)}\n` +
        '\n';

      res.write(message);
      this.sequenceMap.set(connId, nextSeq);
    };

    this.emitter.on('event', listener);

    req.on('close', () => {
      this.emitter.off('event', listener);
      this.sequenceMap.delete(connId);
    });
  }

  broadcast(event, data) {
    this.globalSeq++;
    const eventData = { seq: this.globalSeq, event, data };
    this.eventBuffer.push(eventData);
    if (this.eventBuffer.length > 1000) {
      this.eventBuffer.shift();
    }
    this.emitter.emit('event', eventData);
  }

  getEventBuffer(sinceSeq) {
    return this.eventBuffer.filter(e => e.seq > sinceSeq);
  }
}

const sseServer = new OrderedSSEServer();
http.createServer((req, res) => sseServer.handleConnection(req, res))
  .listen(3000);

// Broadcast events from anywhere
setInterval(() => {
  sseServer.broadcast('update', {
    value: Math.random(),
    time: Date.now(),
  });
}, 1000);

Expected output: Server ensures ordered delivery with sequence numbers, replaying missed events in the correct order.

Common Mistakes

Mistake Explanation
Not sending IDs with events Without IDs, Last-Event-ID is empty, and the client cannot resume from a specific point
Using non-sequential IDs IDs should be monotonically increasing to enable proper resume logic
Not persisting IDs on the client Loss of lastEventId on page reload prevents recovery
Assuming Last-Event-ID is always set On first connection, Last-Event-ID is empty
Sending duplicate events on replay Ensure idempotent event handling or deduplicate on the client

Practice Questions

  1. How does the server receive the last event ID?
  2. How do you generate useful event IDs?
  3. How does the client access the last event ID?
  4. How do you implement event replay on the server?
  5. How do you persist the last event ID across page reloads?

Challenge

Build a reliable SSE system for a notification service. Events must be delivered exactly once, in order, with no data loss on reconnection. Include server-side event buffering with configurable retention, and client-side deduplication.

FAQ

What format should event IDs use?

Event IDs are strings. Common formats include sequential integers, UUIDs, or timestamps. Sequential values work best for recovery.

Can I use timestamps as event IDs?

Yes, but ensure timestamps are monotonically increasing and unique. Combine with a counter for high-frequency events.

What happens if the server cannot replay events?

The client will miss those events. Set appropriate event buffer sizes and consider fallback API calls for missed data.

Does Last-Event-ID work with multiple EventSources?

Each EventSource tracks its own lastEventId independently. Different connections have different resume points.

How much memory does event buffering consume?

Each event stores its ID, type, and data. For 1000 events of ~200 bytes each, memory usage is approximately 200KB.

Can I reset the last event ID?

Yes, the client can close the EventSource and create a new one without sending Last-Event-ID to start fresh.

Mini Project

Build a reliable event streaming system for a stock ticker. The server buffers the last 10,000 events with sequential IDs. Clients recover missed events on reconnection. Include a status indicator showing the last received event ID and any gaps in delivery.

What's Next

Learn about CORS configuration for SSE

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro