Choosing the Right Approach
The decision tree
Do you need the client to send frequent messages to the server?
│
├─ Yes → WebSocket
│ Examples: chat, collaborative editing, games, typing indicators
│
└─ No → Does the server push updates to the client?
│
├─ Yes → SSE
│ Examples: notifications, live feeds, dashboards, log streaming
│
└─ No → REST (no real-time needed)
Examples: CRUD, reports, settings If both sides send frequent messages: WebSocket. If only the server pushes: SSE. If neither side needs push: REST.
The hybrid approach
Most production apps use a combination. Our task board uses:
REST for CRUD operations (create task, update task, delete task). These are one-off actions that need request-response semantics: validation errors, status codes, created resource in the response.
SSE for passive viewers. A user viewing a board receives task updates via SSE. They do not need to send anything — they just watch. SSE’s automatic reconnection and event IDs make this reliable.
WebSocket for interactive features. Presence (who is online), typing indicators, and real-time cursor positions require bidirectional communication. The client sends “I am typing” and receives “Bob is typing.”
This is the pattern used by apps like Notion, Linear, and Figma: REST for mutations, real-time channels for updates and collaboration.
Fallback chains
Not every client supports every protocol. Plan for graceful degradation:
Best: WebSocket
↓ fail: SSE
↓ fail: Long polling
↓ fail: Short polling // Client-side fallback
function connectRealTime(boardId: string) {
// Try WebSocket first
try {
const ws = new WebSocket(`ws://localhost:3000/ws`);
ws.onopen = () => {
ws.send(JSON.stringify({ type: "join", boardId }));
};
ws.onmessage = handleEvent;
ws.onerror = () => fallbackToSSE(boardId);
return;
} catch {
// WebSocket not supported
}
fallbackToSSE(boardId);
}
function fallbackToSSE(boardId: string) {
try {
const source = new EventSource(`/boards/${boardId}/events`);
source.onmessage = handleEvent;
source.onerror = () => fallbackToPolling(boardId);
return;
} catch {
// SSE not supported
}
fallbackToPolling(boardId);
}
function fallbackToPolling(boardId: string) {
setInterval(async () => {
const res = await fetch(`/boards/${boardId}/updates?since=${lastCheck}`);
const { data } = await res.json();
for (const item of data) handleEvent({ data: JSON.stringify(item) });
}, 5000);
} In practice, WebSocket and SSE are supported by all modern browsers. Fallbacks are mainly for enterprise environments with restrictive proxies.
Choosing for common use cases
Chat application: WebSocket. Both sides send messages. Typing indicators. Presence. Low latency.
Live dashboard: SSE. Server pushes metrics and updates. The client only displays. Automatic reconnection.
Collaborative document editing: WebSocket. Both sides send edits. Cursor positions. Conflict resolution requires bidirectional real-time.
Notification feed: SSE. Server pushes notifications. Client reads and dismisses. One-way.
Multiplayer game: WebSocket. Low-latency bidirectional. Binary data for game state. Custom protocols.
Stock ticker: SSE. Server pushes price updates. Client displays. High frequency but one-way.
Exercises
Exercise 1: List every real-time feature in your task board. For each, decide: SSE or WebSocket? (Task updates: SSE. Presence: WebSocket. Typing: WebSocket.)
Exercise 2: Implement the fallback chain. Disable WebSocket support (do not start the WS server). Verify the client falls back to SSE.
Exercise 3: Think about an app you use daily. What real-time features does it have? What protocol do you think it uses?
When should you use SSE instead of WebSockets?