Background pattern
New

WebSockets vs Server-Sent Events

When building real-time applications, choosing between WebSockets and Server-Sent Events (SSE) is crucial. This guide explains the differences, use cases, and when to use each technology.

websocketsssereal-timehttpapi

When building real-time applications that require server-to-client communication, two technologies stand out: WebSockets and Server-Sent Events (SSE). Both allow servers to push data to clients in real-time, but they have different architectures, capabilities, and ideal use cases.

NOTE: This post assumes you have a basic understanding of HTTP, JavaScript, and web development concepts.

## What Are WebSockets?

WebSockets provide a full-duplex communication channel over a single TCP connection. Unlike HTTP, which is request-response based, WebSockets allow both the client and server to send messages at any time without needing to request anything.

### Key Characteristics

  • Full-duplex: Both client and server can send messages simultaneously
  • Persistent connection: Once established, the connection stays open
  • Low latency: No overhead of HTTP headers for each message
  • Binary data support: Efficient for sending binary data like images or files

### WebSocket Connection Flow

### WebSocket Example (Client)

websocket-client-functional.tsx
import { useState, useEffect, useCallback, useRef } from 'react';

function useWebSocket(url: string) {
  const [isConnected, setIsConnected] = useState(false);
  const [lastMessage, setLastMessage] = useState<string | null>(null);
  const wsRef = useRef<WebSocket | null>(null);

  const connect = useCallback(() => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      return;
    }

    const ws = new WebSocket(url);

    ws.onopen = () => {
      console.log('WebSocket connected');
      setIsConnected(true);
    };

    ws.onmessage = (event) => {
      console.log('Received:', event.data);
      setLastMessage(event.data);
    };

    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    ws.onclose = () => {
      console.log('WebSocket disconnected');
      setIsConnected(false);
      // Auto-reconnect logic here
    };

    wsRef.current = ws;
  }, [url]);

  const send = useCallback((message: string | object) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      const data = typeof message === 'string' ? message : JSON.stringify(message);
      wsRef.current.send(data);
    } else {
      console.error('WebSocket is not connected');
    }
  }, []);

  const disconnect = useCallback(() => {
    wsRef.current?.close();
  }, []);

  useEffect(() => {
    connect();

    return () => {
      disconnect();
    };
  }, [connect, disconnect]);

  return { isConnected, lastMessage, send, disconnect, connect };
}

// Usage in a component
function WebSocketComponent() {
  const { isConnected, lastMessage, send } = useWebSocket('ws://localhost:8080');

  return (
    <div>
      <p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
      <p>Last message: {lastMessage}</p>
      <button onClick={() => send({ type: 'greeting', message: 'Hello Server!' })}>
        Send Message
      </button>
    </div>
  );
}
websocket-client-class.tsx
class WebSocketClient {
  private ws: WebSocket | null = null;
  private url: string;

  constructor(url: string) {
    this.url = url;
  }

  connect(): void {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('WebSocket connected');
    };

    this.ws.onmessage = (event) => {
      console.log('Received:', event.data);
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    this.ws.onclose = () => {
      console.log('WebSocket disconnected');
      // Auto-reconnect logic here
    };
  }

  send(message: string | object): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      const data = typeof message === 'string' ? message : JSON.stringify(message);
      this.ws.send(data);
    } else {
      console.error('WebSocket is not connected');
    }
  }

  disconnect(): void {
    this.ws?.close();
  }
}

// Usage
const client = new WebSocketClient('ws://localhost:8080');
client.connect();
client.send({ type: 'greeting', message: 'Hello Server!' });

## What Are Server-Sent Events?

Server-Sent Events (SSE) is a server-push technology that enables a client to receive automatic updates from a server via an HTTP connection. Unlike WebSockets, SSE is unidirectional - only the server can send messages to the client.

### Key Characteristics

  • Unidirectional: Only server can send messages to client
  • Uses HTTP: Built on top of standard HTTP
  • Automatic reconnection: Built-in reconnection handling
  • Text-based: Only supports text data (JSON, plain text)
  • Simple API: Easier to implement than WebSockets

### SSE Connection Flow

### SSE Example (Client)

sse-client-functional.tsx
import { useState, useEffect, useCallback, useRef } from 'react';

function useEventSource(url: string) {
  const [isConnected, setIsConnected] = useState(false);
  const [lastMessage, setLastMessage] = useState<any>(null);
  const [notifications, setNotifications] = useState<any[]>([]);
  const eventSourceRef = useRef<EventSource | null>(null);

  const connect = useCallback(() => {
    if (eventSourceRef.current) {
      return;
    }

    const eventSource = new EventSource(url);

    eventSource.onopen = () => {
      console.log('SSE connected');
      setIsConnected(true);
    };

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      console.log('Received:', data);
      setLastMessage(data);
    };

    eventSource.onerror = (error) => {
      console.error('SSE error:', error);
      // Browser automatically attempts reconnection
    };

    // Listen for specific event types
    eventSource.addEventListener('notification', (event) => {
      const data = JSON.parse((event as MessageEvent).data);
      console.log('Notification:', data);
      setNotifications((prev) => [...prev, data]);
    });

    eventSourceRef.current = eventSource;
  }, [url]);

  const disconnect = useCallback(() => {
    eventSourceRef.current?.close();
    eventSourceRef.current = null;
    setIsConnected(false);
  }, []);

  useEffect(() => {
    connect();

    return () => {
      disconnect();
    };
  }, [connect, disconnect]);

  return { isConnected, lastMessage, notifications, disconnect, connect };
}

// Usage in a component
function SSEComponent() {
  const { isConnected, lastMessage, notifications } = useEventSource('http://localhost:8080/events');

  return (
    <div>
      <p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
      <p>Last message: {JSON.stringify(lastMessage)}</p>
      <ul>
        {notifications.map((n, i) => (
          <li key={i}>{JSON.stringify(n)}</li>
        ))}
      </ul>
    </div>
  );
}
sse-client-class.tsx
class SSEClient {
  private eventSource: EventSource | null = null;
  private url: string;

  constructor(url: string) {
    this.url = url;
  }

  connect(): void {
    this.eventSource = new EventSource(this.url);

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

    this.eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      console.log('Received:', data);
    };

    this.eventSource.onerror = (error) => {
      console.error('SSE error:', error);
      // Browser automatically attempts reconnection
    };

    // Listen for specific event types
    this.eventSource.addEventListener('notification', (event) => {
      const data = JSON.parse(event.data);
      console.log('Notification:', data);
    });
  }

  disconnect(): void {
    this.eventSource?.close();
  }
}

// Usage
const client = new SSEClient('http://localhost:8080/events');
client.connect();

## Key Differences Comparison

FeatureWebSocketsServer-Sent Events
DirectionFull-duplex (bidirectional)Unidirectional (server → client only)
ProtocolWebSocket protocol (ws:// or wss://)HTTP/HTTPS
ReconnectionManual implementation requiredBuilt-in automatic reconnection
Data TypesText and binary dataText only (JSON, plain text)
Browser SupportExcellent (all modern browsers)Excellent (all modern browsers)
Proxy/FirewallMay have issues with some proxiesWorks seamlessly with HTTP
Server ComplexityHigher (requires stateful connection)Lower (can use standard HTTP)
ScalabilityMore complex scalingEasier to scale with HTTP infrastructure
LatencyLower overheadSlightly higher overhead
Use CaseReal-time collaboration, gaming, chatNews feeds, stock prices, notifications

## When to Use WebSockets

### Real-Time Collaboration

WebSockets are ideal for applications where multiple users interact simultaneously:

collaboration-example.tsx
// Real-time document editing
class DocumentCollaborator {
  private ws: WebSocket;

  constructor(docId: string) {
    this.ws = new WebSocket(`ws://localhost:8080/collab/${docId}`);

    this.ws.onmessage = (event) => {
      const update = JSON.parse(event.data);

      switch (update.type) {
        case 'text_insert':
          this.handleTextInsert(update);
          break;
        case 'text_delete':
          this.handleTextDelete(update);
          break;
        case 'cursor_move':
          this.handleCursorMove(update);
          break;
      }
    };
  }

  sendChange(change: object): void {
    this.ws.send(JSON.stringify(change));
  }

  // Other methods...
}

### Chat Applications

Chat applications require bidirectional communication:

chat-functional.tsx
import { useState, useEffect, useCallback, useRef } from 'react';

interface ChatMessage {
  id: string;
  user: string;
  text: string;
  timestamp: number;
}

function useChat(roomId: string) {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [isConnected, setIsConnected] = useState(false);
  const wsRef = useRef<WebSocket | null>(null);

  const connect = useCallback(() => {
    const ws = new WebSocket(`ws://localhost:8080/chat/${roomId}`);

    ws.onopen = () => {
      setIsConnected(true);
    };

    ws.onmessage = (event) => {
      const message: ChatMessage = JSON.parse(event.data);
      setMessages((prev) => [...prev, message]);
    };

    ws.onclose = () => {
      setIsConnected(false);
    };

    wsRef.current = ws;
  }, [roomId]);

  const sendMessage = useCallback((text: string) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(
        JSON.stringify({
          type: 'message',
          text,
          timestamp: Date.now()
        })
      );
    }
  }, []);

  useEffect(() => {
    connect();

    return () => {
      wsRef.current?.close();
    };
  }, [connect]);

  return { messages, isConnected, sendMessage };
}

// Usage
function ChatRoom() {
  const { messages, isConnected, sendMessage } = useChat('room123');

  return (
    <div>
      <div>Connected: {isConnected ? 'Yes' : 'No'}</div>
      <ul>
        {messages.map((m) => (
          <li key={m.id}>
            {m.user}: {m.text}
          </li>
        ))}
      </ul>
    </div>
  );
}
chat-class.tsx
interface ChatMessage {
  id: string;
  user: string;
  text: string;
  timestamp: number;
}

class ChatClient {
  private ws: WebSocket;

  connect(roomId: string): void {
    this.ws = new WebSocket(`ws://localhost:8080/chat/${roomId}`);

    this.ws.onmessage = (event) => {
      const message: ChatMessage = JSON.parse(event.data);
      this.displayMessage(message);
    };
  }

  sendMessage(text: string): void {
    this.ws.send(
      JSON.stringify({
        type: 'message',
        text,
        timestamp: Date.now()
      })
    );
  }

  private displayMessage(message: ChatMessage): void {
    // Display message logic
  }
}

### Online Gaming

Games require low-latency, bidirectional communication:

gaming-functional.tsx
import { useState, useEffect, useCallback, useRef } from 'react';

interface GameState {
  players: PlayerState[];
  ball: BallState;
  score: Score;
}

interface PlayerAction {
  type: string;
  data: any;
}

function useGameClient(gameId: string) {
  const [gameState, setGameState] = useState<GameState | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const wsRef = useRef<WebSocket | null>(null);

  const connect = useCallback(() => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      return;
    }

    const ws = new WebSocket(`ws://localhost:8080/game/${gameId}`);

    ws.onopen = () => {
      console.log('Game connected');
      setIsConnected(true);
    };

    ws.onmessage = (event) => {
      const state: GameState = JSON.parse(event.data);
      setGameState(state);
    };

    ws.onerror = (error) => {
      console.error('Game WebSocket error:', error);
    };

    ws.onclose = () => {
      console.log('Game disconnected');
      setIsConnected(false);
    };

    wsRef.current = ws;
  }, [gameId]);

  const sendAction = useCallback((action: PlayerAction) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify(action));
    } else {
      console.error('Game WebSocket is not connected');
    }
  }, []);

  const disconnect = useCallback(() => {
    wsRef.current?.close();
  }, []);

  useEffect(() => {
    connect();
    return () => {
      disconnect();
    };
  }, [connect, disconnect]);

  return { gameState, isConnected, sendAction, disconnect, connect };
}

// Usage in a component
function GameBoard({ gameId }: { gameId: string }) {
  const { gameState, isConnected, sendAction } = useGameClient(gameId);

  const handleMove = (direction: string) => {
    sendAction({ type: 'move', data: { direction } });
  };

  return (
    <div>
      <p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
      {gameState && (
        <div>
          <p>Score: {gameState.score}</p>
          <button onClick={() => handleMove('up')}>Up</button>
          <button onClick={() => handleMove('down')}>Down</button>
        </div>
      )}
    </div>
  );
}
gaming-class.tsx
interface GameState {
  players: PlayerState[];
  ball: BallState;
  score: Score;
}

class GameClient {
  private ws: WebSocket;

  connect(gameId: string): void {
    this.ws = new WebSocket(`ws://localhost:8080/game/${gameId}`);

    this.ws.onmessage = (event) => {
      const state: GameState = JSON.parse(event.data);
      this.render(state);
    };
  }

  sendAction(action: PlayerAction): void {
    this.ws.send(JSON.stringify(action));
  }
}

## When to Use Server-Sent Events

### Live Feeds and Updates

SSE is perfect for one-way data streams like live feeds:

live-feed-functional.tsx
import { useState, useEffect, useCallback, useRef } from 'react';

function useLiveFeed(feedType: string) {
  const [feed, setFeed] = useState<any[]>([]);
  const [isConnected, setIsConnected] = useState(false);
  const eventSourceRef = useRef<EventSource | null>(null);

  const subscribe = useCallback(() => {
    if (eventSourceRef.current) {
      return;
    }

    const eventSource = new EventSource(`/api/feeds/${feedType}`);

    eventSource.onopen = () => {
      console.log(`${feedType} feed connected`);
      setIsConnected(true);
    };

    eventSource.onmessage = (event) => {
      const update = JSON.parse(event.data);
      setFeed((prev) => [...prev, update]);
    };

    eventSource.onerror = (error) => {
      console.error('Feed error:', error);
    };

    eventSourceRef.current = eventSource;
  }, [feedType]);

  const unsubscribe = useCallback(() => {
    eventSourceRef.current?.close();
    eventSourceRef.current = null;
    setIsConnected(false);
  }, []);

  useEffect(() => {
    subscribe();
    return () => {
      unsubscribe();
    };
  }, [subscribe, unsubscribe]);

  return { feed, isConnected, unsubscribe, subscribe };
}

// Usage for stock prices
function StockTicker() {
  const { feed } = useLiveFeed('stock-prices');

  return (
    <div>
      <h2>Stock Prices</h2>
      <ul>
        {feed.map((item, i) => (
          <li key={i}>{JSON.stringify(item)}</li>
        ))}
      </ul>
    </div>
  );
}

// Usage for news updates
function NewsFeed() {
  const { feed } = useLiveFeed('news');

  return (
    <div>
      <h2>News Updates</h2>
      <ul>
        {feed.map((item, i) => (
          <li key={i}>{JSON.stringify(item)}</li>
        ))}
      </ul>
    </div>
  );
}
live-feed-class.tsx
class LiveFeedClient {
  private eventSource: EventSource;

  subscribe(feedType: string): void {
    this.eventSource = new EventSource(`/api/feeds/${feedType}`);

    this.eventSource.onmessage = (event) => {
      const update = JSON.parse(event.data);
      this.updateFeed(update);
    };
  }
}

// Usage for stock prices
const stockFeed = new LiveFeedClient();
stockFeed.subscribe('stock-prices');

// Usage for news updates
const newsFeed = new LiveFeedClient();
newsFeed.subscribe('news');

### Notification Systems

SSE excels at delivering notifications:

notification-functional.tsx
import { useState, useEffect, useCallback, useRef } from 'react';

interface Notification {
  id: string;
  type: 'info' | 'warning' | 'error' | 'success';
  title: string;
  message: string;
  timestamp: number;
}

function useNotifications(userId: string) {
  const [notifications, setNotifications] = useState<Notification[]>([]);
  const eventSourceRef = useRef<EventSource | null>(null);

  const connect = useCallback(() => {
    if (eventSourceRef.current) {
      return;
    }

    const eventSource = new EventSource(`/api/notifications/${userId}`);

    eventSource.addEventListener('notification', (event) => {
      const notification: Notification = JSON.parse((event as MessageEvent).data);
      console.log(`[${notification.type}] ${notification.title}: ${notification.message}`);
      setNotifications((prev) => [...prev, notification]);
    });

    eventSource.onerror = (error) => {
      console.error('SSE error:', error);
    };

    eventSourceRef.current = eventSource;
  }, [userId]);

  const disconnect = useCallback(() => {
    eventSourceRef.current?.close();
    eventSourceRef.current = null;
  }, []);

  useEffect(() => {
    connect();
    return () => {
      disconnect();
    };
  }, [connect, disconnect]);

  return { notifications, disconnect, connect };
}

// Usage in a component
function NotificationCenter({ userId }: { userId: string }) {
  const { notifications } = useNotifications(userId);

  return (
    <div>
      <h2>Notifications</h2>
      <ul>
        {notifications.map((n) => (
          <li key={n.id}>
            [{n.type}] {n.title}: {n.message}
          </li>
        ))}
      </ul>
    </div>
  );
}
notification-class.tsx
interface Notification {
  id: string;
  type: 'info' | 'warning' | 'error' | 'success';
  title: string;
  message: string;
  timestamp: number;
}

class NotificationClient {
  private eventSource: EventSource;

  connect(userId: string): void {
    this.eventSource = new EventSource(`/api/notifications/${userId}`);

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

  private showNotification(notification: Notification): void {
    // Display notification to user
    console.log(`[${notification.type}] ${notification.title}: ${notification.message}`);
  }
}

### Progress Updates

SSE is great for long-running operations:

progress-functional.tsx
import { useState, useEffect, useCallback, useRef } from 'react';

interface ProgressUpdate {
  jobId: string;
  progress: number;
  status: string;
  completed: boolean;
}

function useJobProgress(jobId: string) {
  const [progress, setProgress] = useState<ProgressUpdate | null>(null);
  const [isCompleted, setIsCompleted] = useState(false);
  const eventSourceRef = useRef<EventSource | null>(null);

  const trackJob = useCallback(() => {
    if (eventSourceRef.current) {
      return;
    }

    const eventSource = new EventSource(`/api/jobs/${jobId}/progress`);

    eventSource.onmessage = (event) => {
      const update: ProgressUpdate = JSON.parse(event.data);
      setProgress(update);

      if (update.completed) {
        setIsCompleted(true);
        eventSource.close();
        eventSourceRef.current = null;
      }
    };

    eventSource.onerror = (error) => {
      console.error('SSE error:', error);
    };

    eventSourceRef.current = eventSource;
  }, [jobId]);

  const stopTracking = useCallback(() => {
    eventSourceRef.current?.close();
    eventSourceRef.current = null;
  }, []);

  useEffect(() => {
    trackJob();
    return () => {
      stopTracking();
    };
  }, [trackJob, stopTracking]);

  return { progress, isCompleted, stopTracking };
}

// Usage in a component
function JobTracker({ jobId }: { jobId: string }) {
  const { progress, isCompleted } = useJobProgress(jobId);

  return (
    <div>
      <h3>Job Progress</h3>
      {progress ? (
        <div>
          <p>Status: {progress.status}</p>
          <p>Progress: {progress.progress}%</p>
          {isCompleted && <p>Job completed!</p>}
        </div>
      ) : (
        <p>Connecting...</p>
      )}
    </div>
  );
}
progress-class.tsx
interface ProgressUpdate {
  jobId: string;
  progress: number;
  status: string;
  completed: boolean;
}

class ProgressTracker {
  trackJob(jobId: string): void {
    const eventSource = new EventSource(`/api/jobs/${jobId}/progress`);

    eventSource.onmessage = (event) => {
      const update: ProgressUpdate = JSON.parse(event.data);

      this.updateProgressBar(update.progress);

      if (update.completed) {
        eventSource.close();
        this.showCompletionMessage();
      }
    };
  }

  private updateProgressBar(progress: number): void {
    console.log(`Progress: ${progress}%`);
  }

  private showCompletionMessage(): void {
    console.log('Job completed!');
  }
}

## Real-Time Data Streaming

SSE is excellent for streaming real-time data:

streaming-functional.tsx
import { useState, useEffect, useCallback, useRef } from 'react';

function useDataStream(metrics: string[]) {
  const [streamData, setStreamData] = useState<any[]>([]);
  const [isConnected, setIsConnected] = useState(false);
  const eventSourceRef = useRef<EventSource | null>(null);

  const startStream = useCallback(() => {
    if (eventSourceRef.current) {
      return;
    }

    const params = new URLSearchParams({ metrics: metrics.join(',') });
    const eventSource = new EventSource(`/api/stream?${params}`);

    eventSource.onopen = () => {
      console.log('Data stream connected');
      setIsConnected(true);
    };

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setStreamData((prev) => [...prev, data]);
    };

    eventSource.onerror = (error) => {
      console.error('Stream error:', error);
    };

    eventSourceRef.current = eventSource;
  }, [metrics]);

  const stopStream = useCallback(() => {
    eventSourceRef.current?.close();
    eventSourceRef.current = null;
    setIsConnected(false);
  }, []);

  useEffect(() => {
    startStream();
    return () => {
      stopStream();
    };
  }, [startStream, stopStream]);

  return { streamData, isConnected, startStream, stopStream };
}

// Usage in a component
function DataDashboard({ metrics }: { metrics: string[] }) {
  const { streamData, isConnected } = useDataStream(metrics);

  return (
    <div>
      <h2>Real-Time Dashboard</h2>
      <p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
      <ul>
        {streamData.map((data, i) => (
          <li key={i}>{JSON.stringify(data)}</li>
        ))}
      </ul>
    </div>
  );
}
streaming-class.tsx
class DataStreamClient {
  private eventSource: EventSource | null = null;

  startStream(metrics: string[]): void {
    const params = new URLSearchParams({ metrics: metrics.join(',') });
    this.eventSource = new EventSource(`/api/stream?${params}`);

    this.eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      this.updateDashboard(data);
    };
  }

  stopStream(): void {
    this.eventSource?.close();
  }

  private updateDashboard(data: any): void {
    // Update UI with streamed data
  }
}

## Combining Both Technologies

Sometimes the best approach is to use both technologies together based on the use case:

hybrid-functional.tsx
import { useState, useEffect, useCallback, useRef } from 'react';

function useHybridClient(roomId: string, userId: string) {
  const [collabData, setCollabData] = useState<any>(null);
  const [notifications, setNotifications] = useState<any[]>([]);
  const wsRef = useRef<WebSocket | null>(null);
  const eventSourceRef = useRef<EventSource | null>(null);

  // Use WebSocket for real-time collaboration
  const connectCollaboration = useCallback(() => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      return;
    }

    const ws = new WebSocket(`ws://localhost:8080/collab/${roomId}`);

    ws.onopen = () => {
      console.log('Collaboration WebSocket connected');
    };

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setCollabData(data);
    };

    ws.onerror = (error) => {
      console.error('Collaboration WebSocket error:', error);
    };

    wsRef.current = ws;
  }, [roomId]);

  // Use SSE for notifications
  const subscribeNotifications = useCallback(() => {
    if (eventSourceRef.current) {
      return;
    }

    const eventSource = new EventSource(`/api/notifications/${userId}`);

    eventSource.addEventListener('notification', (event) => {
      const notification = JSON.parse((event as MessageEvent).data);
      setNotifications((prev) => [...prev, notification]);
    });

    eventSourceRef.current = eventSource;
  }, [userId]);

  const disconnectAll = useCallback(() => {
    wsRef.current?.close();
    eventSourceRef.current?.close();
    eventSourceRef.current = null;
  }, []);

  useEffect(() => {
    connectCollaboration();
    subscribeNotifications();
    return () => {
      disconnectAll();
    };
  }, [connectCollaboration, subscribeNotifications, disconnectAll]);

  return { collabData, notifications, disconnectAll };
}

// Usage in a component
function HybridApp({ roomId, userId }: { roomId: string; userId: string }) {
  const { collabData, notifications } = useHybridClient(roomId, userId);

  return (
    <div>
      <h2>Collaborative Room</h2>
      {collabData && <div>Collab Data: {JSON.stringify(collabData)}</div>}

      <h3>Notifications</h3>
      <ul>
        {notifications.map((n, i) => (
          <li key={i}>{JSON.stringify(n)}</li>
        ))}
      </ul>
    </div>
  );
}
hybrid-class.tsx
class HybridClient {
  private ws: WebSocket | null = null;
  private eventSource: EventSource | null = null;

  // Use WebSocket for real-time collaboration
  connectCollaboration(roomId: string): void {
    this.ws = new WebSocket(`ws://localhost:8080/collab/${roomId}`);
    this.ws.onmessage = (event) => this.handleCollaboration(event);
  }

  // Use SSE for notifications
  subscribeNotifications(userId: string): void {
    this.eventSource = new EventSource(`/api/notifications/${userId}`);
    this.eventSource.addEventListener('notification', (event) => {
      this.handleNotification(event);
    });
  }

  private handleCollaboration(event: MessageEvent): void {
    // Handle collaboration updates
  }

  private handleNotification(event: MessageEvent): void {
    // Handle notifications
  }
}

## Server-Side Examples

### Node.js WebSocket Server

websocket-server.ts
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('Client connected');

  ws.on('message', (data) => {
    // Broadcast to all clients
    wss.clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data);
      }
    });
  });

  ws.on('close', () => {
    console.log('Client disconnected');
  });
});

### Node.js SSE Server

sse-server.ts
import express from 'express';

const app = express();

app.get('/events', (req, res) => {
  // Set headers for SSE
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // Send initial message
  res.write('event: connected\ndata: {"message": "Connected!"}\n\n');

  // Send updates every second
  const interval = setInterval(() => {
    const data = JSON.stringify({
      timestamp: Date.now(),
      value: Math.random()
    });
    res.write(`data: ${data}\n\n`);
  }, 1000);

  // Clean up on client disconnect
  req.on('close', () => {
    clearInterval(interval);
  });
});

app.listen(8080);

## Decision Framework

Use this decision tree to choose between WebSockets and SSE:

Do you need client → server communication?
├── Yes → Use WebSockets
└── No → Continue

    Do you need binary data support?
    ├── Yes → Use WebSockets
    └── No → Continue

        Is simplicity more important than lowest latency?
        ├── Yes → Use SSE
        └── No → Consider WebSockets

## Conclusion

Both WebSockets and Server-Sent Events are powerful technologies for real-time communication:

  • Choose WebSockets for interactive, bidirectional applications like chat, collaboration, and gaming
  • Choose SSE for one-way data streaming like notifications, live feeds, and updates

Consider your specific requirements: communication direction, data types, complexity, and scalability needs. Often, the right choice becomes clear when you evaluate what you're trying to achieve rather than choosing based on technical features alone.

Published on January 12, 2026

17 min read

Found an Issue!

Find an issue with this post? Think you could clarify, update or add something? All my posts are available to edit on Github. Any fix, little or small, is appreciated!

Edit on GitHub

Last updated on