MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Server-Sent Events (SSE): Simple Real-Time Communication

Implement SSE: event streams, retry mechanisms, and when to use SSE over WebSocket.

SSEReal-TimeServer-Sent EventsBackend

By MinhVo

Introduction

In the ever-evolving landscape of web development, real-time communication has become a cornerstone of modern applications. Whether you're building live dashboards, notification systems, or collaborative tools, users expect instant updates without manual page refreshes. While WebSocket has long been the go-to solution for bidirectional communication, Server-Sent Events (SSE) offers a simpler, more efficient alternative for scenarios where data flows primarily from server to client.

SSE provides a standardized mechanism for servers to push updates to clients over a single HTTP connection. Unlike WebSocket's full-duplex communication, SSE operates as a unidirectional channel, making it ideal for applications like live feeds, stock tickers, and event notifications where the client primarily receives data rather than sending it.

Real-time data streaming

Understanding Server-Sent Events: Core Concepts

Server-Sent Events is an HTML5 specification that enables servers to push data to clients automatically. The protocol operates over standard HTTP, which means it works seamlessly with existing infrastructure, proxies, and firewalls. Unlike WebSocket, which requires a separate protocol upgrade, SSE uses the familiar HTTP/1.1 or HTTP/2 connections.

The core advantage of SSE lies in its simplicity. The client creates an EventSource object, and the server maintains an open connection, sending text-based events as they occur. Each event contains an optional event type, data payload, and unique identifier for reconnection purposes.

SSE excels in scenarios where:

  • Data flows primarily from server to client
  • You need automatic reconnection handling
  • You want to leverage HTTP caching and compression
  • Browser compatibility is a priority
  • You need to maintain a simple architecture without bidirectional complexity

The protocol supports three key features: automatic reconnection with configurable retry intervals, event typing for handling different message categories, and last-event-id tracking for resuming streams after disconnections.

Data streaming architecture

Architecture and Design Patterns

The Event Stream Protocol

SSE communication follows a straightforward architecture. The client initiates a standard HTTP GET request to a specific endpoint, and the server responds with a Content-Type: text/event-stream header. The connection remains open, allowing the server to send multiple events over time.

Each event in the stream follows a specific format:

event: message-type
id: 123
retry: 5000
data: {"key": "value"}

The protocol supports four field types:

  • event: Optional type name for the event
  • data: The actual payload (can span multiple lines)
  • id: Unique identifier for reconnection tracking
  • retry: Reconnection interval in milliseconds

Connection Management Patterns

Modern SSE implementations typically follow several architectural patterns:

Simple Streaming: Direct event emission from server to client, ideal for basic notification systems. The server maintains a list of connected clients and broadcasts events as they occur.

Channel-based Pub/Sub: Events are organized into channels or topics, allowing clients to subscribe to specific streams of interest. This pattern reduces bandwidth by delivering only relevant data.

Filtered Streams: Servers apply client-specific filters before sending events, ensuring each user receives personalized content without over-fetching.

Fan-out Architecture: A single event source distributes messages to multiple clients through a centralized event bus, enabling efficient scaling across distributed systems.

Integration with Modern Frameworks

Most backend frameworks provide built-in support or middleware for SSE:

Node.js with Express: Using the response.write() method with proper headers creates persistent streams. Libraries like express-sse simplify the implementation further.

Python with FastAPI: FastAPI's StreamingResponse combined with async generators provides elegant SSE support with automatic connection management.

Go with standard library: Go's net/http package handles SSE natively through http.Flusher interfaces, offering excellent performance for high-throughput scenarios.

Server architecture diagram

Step-by-Step Implementation

Let's implement a complete SSE system with a Node.js server and client-side integration.

Setting Up the Server

// server.js - Express SSE implementation
const express = require('express');
const cors = require('cors');
 
const app = express();
app.use(cors());
 
// Store connected clients
const clients = new Set();
 
// SSE endpoint
app.get('/events', (req, res) => {
  // Set SSE headers
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Access-Control-Allow-Origin': '*'
  });
 
  // Send initial connection event
  res.write('event: connected\ndata: {"status":"connected"}\n\n');
 
  // Add client to tracking set
  clients.add(res);
 
  // Remove client on disconnect
  req.on('close', () => {
    clients.delete(res);
    console.log(`Client disconnected. Total: ${clients.size}`);
  });
 
  console.log(`New client connected. Total: ${clients.size}`);
});
 
// Broadcast function
function broadcast(eventType, data) {
  const message = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
  clients.forEach(client => {
    client.write(message);
  });
}
 
// Example: Broadcast stock updates every 2 seconds
setInterval(() => {
  const stockUpdate = {
    symbol: 'AAPL',
    price: (150 + Math.random() * 10).toFixed(2),
    change: (Math.random() * 2 - 1).toFixed(2),
    timestamp: new Date().toISOString()
  };
  broadcast('stock-update', stockUpdate);
}, 2000);
 
app.listen(3001, () => {
  console.log('SSE server running on port 3001');
});

Client-Side Implementation

// client.js - Browser EventSource implementation
class SSEClient {
  constructor(url) {
    this.url = url;
    this.eventSource = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.handlers = new Map();
  }
 
  connect() {
    this.eventSource = new EventSource(this.url);
 
    this.eventSource.onopen = () => {
      console.log('SSE connection established');
      this.reconnectAttempts = 0;
    };
 
    this.eventSource.onerror = (error) => {
      console.error('SSE error:', error);
      if (this.eventSource.readyState === EventSource.CLOSED) {
        this.handleReconnect();
      }
    };
 
    // Register event handlers
    this.handlers.forEach((handler, eventType) => {
      this.eventSource.addEventListener(eventType, (event) => {
        const data = JSON.parse(event.data);
        handler(data, event);
      });
    });
 
    return this;
  }
 
  on(eventType, handler) {
    this.handlers.set(eventType, handler);
    if (this.eventSource) {
      this.eventSource.addEventListener(eventType, (event) => {
        const data = JSON.parse(event.data);
        handler(data, event);
      });
    }
    return this;
  }
 
  handleReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
      console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
      setTimeout(() => this.connect(), delay);
    } else {
      console.error('Max reconnection attempts reached');
    }
  }
 
  disconnect() {
    if (this.eventSource) {
      this.eventSource.close();
      this.eventSource = null;
    }
  }
}
 
// Usage
const sse = new SSEClient('http://localhost:3001/events')
  .on('connected', (data) => {
    console.log('Connected to SSE server:', data);
  })
  .on('stock-update', (data) => {
    document.getElementById('stock-price').textContent = 
      `${data.symbol}: $${data.price} (${data.change > 0 ? '+' : ''}${data.change}%)`;
  })
  .connect();

Handling Reconnection and Last Event ID

// Enhanced server with Last-Event-ID support
app.get('/events', (req, res) => {
  const lastEventId = req.headers['last-event-id'];
  
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });
 
  // If reconnecting, send missed events
  if (lastEventId) {
    const missedEvents = getMissedEvents(lastEventId);
    missedEvents.forEach(event => {
      res.write(`id: ${event.id}\nevent: ${event.type}\ndata: ${event.data}\n\n`);
    });
  }
 
  // Continue with normal event streaming...
});

Implementation workflow

Real-World Use Cases and Case Studies

Live Dashboard Updates

Financial platforms like Bloomberg Terminal and Yahoo Finance leverage SSE for real-time stock price updates. The unidirectional nature of SSE perfectly matches this use case—prices flow from server to thousands of clients simultaneously without requiring client-to-server communication for each update.

A typical implementation processes market data feeds, applies real-time calculations, and broadcasts updates to subscribed clients within milliseconds. The automatic reconnection ensures users see continuous price movements even during temporary network interruptions.

Notification Systems

Modern web applications use SSE to deliver instant notifications without polling. Services like GitHub, Slack, and Trello implement notification streams that alert users to mentions, messages, or task updates. SSE's support for event types allows distinguishing between notification categories (mention, message, system alert) on a single connection.

Live Content Feeds

News websites and social media platforms employ SSE for streaming live content updates. Twitter's timeline, Reddit's live threads, and news tickers all benefit from SSE's ability to push content as it becomes available. The protocol's text-based nature makes it ideal for delivering structured content like articles, comments, and status updates.

Collaborative Editing

Applications like Google Docs and Notion use SSE to broadcast document changes to all editors in real-time. When one user modifies content, the change propagates to all other participants through SSE streams, enabling seamless collaborative editing without constant polling.

IoT Data Visualization

Industrial monitoring systems and smart home dashboards collect sensor data through SSE connections. Temperature readings, device statuses, and performance metrics stream to visualization interfaces, providing operators with real-time insights into system health.

Best Practices for Production

  1. Implement Proper Error Handling: Always handle connection errors gracefully. Implement exponential backoff for reconnection attempts to avoid overwhelming servers during outages.

  2. Use Event Types Strategically: Organize events into logical categories using the event field. This allows clients to subscribe to specific event types and simplifies frontend event handling.

  3. Set Appropriate Retry Intervals: Configure the retry field based on your application's requirements. Lower values provide faster reconnection but increase server load during outages.

  4. Implement Heartbeat Messages: Send periodic heartbeat events to detect connection health and prevent proxy timeouts. A simple ping every 30 seconds keeps connections alive through corporate firewalls.

  5. Use Last-Event-ID for Reliability: Always include event IDs to enable reliable reconnection. Clients can resume from where they left off instead of missing events during disconnections.

  6. Compress Event Streams: Enable gzip or Brotli compression for SSE streams to reduce bandwidth usage, especially when sending large payloads frequently.

  7. Implement Client-Side Debouncing: For high-frequency events, debounce updates on the client to prevent UI performance issues. Batch rapid updates before rendering.

  8. Monitor Connection Counts: Track active SSE connections to prevent resource exhaustion. Implement connection limits per client and monitor server memory usage.

  9. Use HTTP/2 When Possible: HTTP/2's multiplexing capabilities allow multiple SSE streams over a single TCP connection, reducing latency and improving resource utilization.

  10. Implement Graceful Degradation: Provide fallback mechanisms like long-polling for environments where SSE isn't supported, ensuring consistent user experience across platforms.

Common Pitfalls and Solutions

PitfallImpactSolution
No reconnection handlingUsers lose connection permanently after network issuesImplement automatic reconnection with exponential backoff and Last-Event-ID tracking
Proxy timeout disconnectsCorporate proxies close idle connections after 60 secondsSend heartbeat messages every 30 seconds to maintain connection activity
Memory leaks from event listenersApplication performance degrades over timeAlways remove event listeners when components unmount using cleanup functions
CORS misconfigurationCross-origin requests blocked by browserConfigure proper CORS headers including Access-Control-Allow-Origin
No connection poolingServer resource exhaustion with many clientsImplement connection limits and use event buses to fan out messages efficiently
Binary data transmissionSSE only supports UTF-8 textBase64-encode binary data or use alternative protocols for binary streams
Single connection per domainBrowser limits SSE connections per domainUse HTTP/2 multiplexing or implement channel multiplexing on a single connection

Performance Optimization

SSE performance optimization focuses on minimizing latency and resource consumption while maintaining reliability.

// Connection pooling with Redis pub/sub
const Redis = require('ioredis');
const redis = new Redis();
 
// Subscribe to Redis channel
redis.subscribe('events', (err, count) => {
  console.log(`Subscribed to ${count} channel(s)`);
});
 
// Broadcast from Redis messages
redis.on('message', (channel, message) => {
  const data = JSON.parse(message);
  broadcast(data.type, data.payload);
});
 
// Optimized broadcast with batching
let pendingEvents = [];
let batchTimeout = null;
 
function batchBroadcast(eventType, data) {
  pendingEvents.push({ type: eventType, data });
  
  if (!batchTimeout) {
    batchTimeout = setTimeout(() => {
      const batchMessage = pendingEvents
        .map(e => `event: ${e.type}\ndata: ${JSON.stringify(e.data)}\n\n`)
        .join('');
      
      clients.forEach(client => client.write(batchMessage));
      pendingEvents = [];
      batchTimeout = null;
    }, 16); // ~60fps batching
  }
}

Comparison with Alternatives

FeatureSSEWebSocketLong PollingWebTransport
DirectionServer → ClientBidirectionalServer → ClientBidirectional
ProtocolHTTP/1.1, HTTP/2WS, WSSHTTP/1.1HTTP/3
Automatic ReconnectionBuilt-inManualManualManual
Binary SupportText onlyYesText onlyYes
Proxy CompatibilityExcellentVariableExcellentGood
Browser SupportAll modernAll modernAll modernLimited
Implementation ComplexityLowMediumMediumHigh
Connection OverheadMinimalModerateHighMinimal
ScalabilityHighMediumLowHigh

SSE is the optimal choice when you need reliable server-to-client streaming with minimal implementation complexity. WebSocket suits applications requiring frequent client-to-server communication. Long polling serves as a fallback for legacy systems, while WebTransport offers cutting-edge bidirectional streaming for modern browsers.

Advanced Patterns and Techniques

Event Stream Multiplexing

// Multiplex multiple data streams on single SSE connection
const streams = {
  stocks: new EventEmitter(),
  news: new EventEmitter(),
  alerts: new EventEmitter()
};
 
app.get('/events', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });
 
  // Subscribe to multiple streams
  Object.entries(streams).forEach(([name, emitter]) => {
    emitter.on('data', (data) => {
      res.write(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
    });
  });
 
  req.on('close', () => {
    // Cleanup listeners
    Object.values(streams).forEach(emitter => {
      emitter.removeAllListeners('data');
    });
  });
});

Server-Sent Events with GraphQL Subscriptions

// GraphQL subscription resolver using SSE
const resolvers = {
  Subscription: {
    messageAdded: {
      subscribe: async function* () {
        const messages = [];
        const queue = [];
        
        // Yield messages as they arrive
        while (true) {
          const message = await new Promise(resolve => queue.push(resolve));
          yield { messageAdded: message };
        }
      }
    }
  }
};

Testing Strategies

Comprehensive SSE testing requires covering connection lifecycle, error handling, and message delivery.

// Jest test suite for SSE implementation
describe('SSE Server', () => {
  let server;
  let client;
 
  beforeAll(() => {
    server = startSSEServer(3002);
  });
 
  afterAll(() => {
    server.close();
  });
 
  test('establishes connection and receives events', (done) => {
    const eventSource = new EventSource('http://localhost:3002/events');
    
    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      expect(data).toHaveProperty('timestamp');
      eventSource.close();
      done();
    };
  });
 
  test('handles reconnection with Last-Event-ID', async () => {
    const eventSource = new EventSource('http://localhost:3002/events');
    let receivedId = null;
 
    eventSource.onmessage = (event) => {
      receivedId = event.lastEventId;
      eventSource.close();
    };
 
    // Simulate disconnect and reconnect
    await simulateDisconnect();
    
    expect(receivedId).toBeDefined();
  });
 
  test('receives typed events', (done) => {
    const eventSource = new EventSource('http://localhost:3002/events');
    
    eventSource.addEventListener('notification', (event) => {
      const data = JSON.parse(event.data);
      expect(data.type).toBe('notification');
      eventSource.close();
      done();
    });
 
    // Trigger notification event
    triggerNotification();
  });
});

Future Outlook

Server-Sent Events continue to evolve alongside web standards. The specification receives regular updates, with recent additions including improved error handling and enhanced security features. Browser support remains excellent across all modern platforms, making SSE a reliable choice for production applications.

The rise of edge computing and serverless architectures has sparked renewed interest in SSE's lightweight protocol. Edge providers like Cloudflare Workers and Vercel Edge Functions now offer native SSE support, enabling low-latency streaming from globally distributed infrastructure.

Integration with modern frameworks like React, Vue, and Svelte has improved significantly. Libraries like @microsoft/fetch-event-source and eventsource provide enhanced functionality beyond native EventSource implementations, including POST request support and custom headers.

The TC39 proposal for AsyncIterator integration promises to bring native async iteration to SSE streams, simplifying consumption patterns and enabling seamless integration with modern JavaScript features like for await...of loops.

Conclusion

Server-Sent Events provide an elegant, efficient solution for server-to-client real-time communication. By leveraging standard HTTP connections and offering built-in reconnection capabilities, SSE simplifies the implementation of live data streaming without the complexity of WebSocket's bidirectional protocol.

Key takeaways:

  1. SSE excels in unidirectional streaming scenarios like notifications, dashboards, and live feeds
  2. Automatic reconnection and Last-Event-ID tracking ensure reliable message delivery
  3. HTTP-based protocol enables seamless integration with existing infrastructure and tools
  4. Production implementations require heartbeat monitoring, connection pooling, and proper error handling
  5. Consider SSE when your application primarily needs server-to-client updates without frequent client messages

For developers building real-time applications, SSE offers the perfect balance of simplicity and capability. Start with the native EventSource API for basic implementations, then scale with Redis pub/sub or message queues as your application grows. Explore the official HTML specification and MDN documentation for advanced features and browser compatibility details.