SSE Event IDs and last-event-id — Tracking Events and Recovering from Disconnections
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
- What field assigns an ID to an SSE event?
- How does the browser communicate the last received ID on reconnection?
- What is needed on the server side to support replay?
- How long should you buffer events?
- What happens if the server has no events after the given ID?
Answers
- The
id:field. 2. Via theLast-Event-IDHTTP 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
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