Choosing a Real-time Transport
| Feature | WebSocket | Server-Sent Events | Socket.IO |
|---|---|---|---|
| Direction | Full-duplex (both ways) | Server → Client only | Full-duplex (both ways) |
| Protocol | ws:// / wss:// | HTTP (text/event-stream) | WebSocket + HTTP fallback |
| Reconnection | Manual | Automatic (built-in) | Automatic |
| JSON support | Manual serialization | Text data (parse manually) | Built-in serialization |
| Rooms / Channels | Manual | Not built-in | Built-in rooms |
| Browser support | All modern browsers | All except IE | All (with fallback) |
| Best for | Chat, games, collab | Dashboards, feeds, logs | Complex real-time apps |
JSON Message Envelope Pattern
Before diving into transports, define a standard message format for all real-time communication:
Standard JSON message envelopetypescript
1interface WebSocketMessage<T = unknown> {2 type: string; // Event name: "chat.message", "user.typing", "error"3 payload: T; // Event-specific data4 id?: string; // Message ID for deduplication5 timestamp: string; // ISO 86016}78// Examples9const chatMessage: WebSocketMessage = {10 type: "chat.message",11 payload: { userId: "user_42", text: "Hello!", roomId: "room_1" },12 id: "msg_abc123",13 timestamp: "2026-04-02T14:30:00Z",14};1516const typingEvent: WebSocketMessage = {17 type: "user.typing",18 payload: { userId: "user_42", roomId: "room_1" },19 timestamp: "2026-04-02T14:30:05Z",20};2122const errorEvent: WebSocketMessage = {23 type: "error",24 payload: { code: "RATE_LIMITED", message: "Too many messages" },25 timestamp: "2026-04-02T14:30:10Z",26};Tip
Always include a
type field so the receiver knows how to handle each message. This pattern is used by Slack, Discord, and most real-time platforms.1. Native WebSocket with JSON
Client (Browser)
WebSocket client with JSONtypescript
1const ws = new WebSocket('wss://api.example.com/ws');23ws.addEventListener('open', () => {4 console.log('Connected');5 ws.send(JSON.stringify({6 type: 'auth',7 payload: { token: 'jwt_token_here' },8 timestamp: new Date().toISOString(),9 }));10});1112ws.addEventListener('message', (event) => {13 try {14 const message = JSON.parse(event.data);1516 switch (message.type) {17 case 'chat.message':18 addMessage(message.payload);19 break;20 case 'user.typing':21 showTypingIndicator(message.payload.userId);22 break;23 case 'error':24 handleError(message.payload);25 break;26 default:27 console.warn('Unknown message type:', message.type);28 }29 } catch {30 console.error('Invalid JSON from server:', event.data);31 }32});3334ws.addEventListener('close', (event) => {35 console.log(`Disconnected: ${event.code} ${event.reason}`);36 scheduleReconnect();37});Server (Node.js)
WebSocket server with ws librarytypescript
1import { WebSocketServer, WebSocket } from 'ws';23const wss = new WebSocketServer({ port: 8080 });45wss.on('connection', (ws) => {6 ws.on('message', (raw) => {7 let message;8 try {9 message = JSON.parse(raw.toString());10 } catch {11 ws.send(JSON.stringify({12 type: 'error',13 payload: { code: 'INVALID_JSON', message: 'Message must be valid JSON' },14 timestamp: new Date().toISOString(),15 }));16 return;17 }1819 if (message.type === 'chat.message') {20 // Broadcast to all connected clients21 const broadcast = JSON.stringify({22 type: 'chat.message',23 payload: message.payload,24 id: crypto.randomUUID(),25 timestamp: new Date().toISOString(),26 });2728 wss.clients.forEach((client) => {29 if (client.readyState === WebSocket.OPEN) {30 client.send(broadcast);31 }32 });33 }34 });35});2. Server-Sent Events (SSE) with JSON
Server (Express)
SSE endpointtypescript
1import express from 'express';2const app = express();34app.get('/events', (req, res) => {5 res.writeHead(200, {6 'Content-Type': 'text/event-stream',7 'Cache-Control': 'no-cache',8 'Connection': 'keep-alive',9 });1011 const sendEvent = (type: string, data: unknown) => {12 const json = JSON.stringify({ type, payload: data, timestamp: new Date().toISOString() });13 res.write(`event: ${type}\ndata: ${json}\n\n`);14 };1516 // Send initial data17 sendEvent('connected', { message: 'Stream started' });1819 // Send periodic updates20 const interval = setInterval(() => {21 sendEvent('stock.update', {22 symbol: 'AAPL',23 price: 150 + Math.random() * 10,24 change: (Math.random() - 0.5) * 2,25 });26 }, 2000);2728 req.on('close', () => clearInterval(interval));29});Client (Browser)
EventSource clienttypescript
1const source = new EventSource('/events');23source.addEventListener('stock.update', (event) => {4 const data = JSON.parse(event.data);5 updateStockTicker(data.payload);6});78source.addEventListener('error', () => {9 console.log('SSE connection lost — will auto-reconnect');10});1112// To close manually13// source.close();3. Socket.IO with JSON
Socket.IO servertypescript
1import { Server } from 'socket.io';23const io = new Server(3001, { cors: { origin: '*' } });45io.on('connection', (socket) => {6 console.log('Client connected:', socket.id);78 socket.on('chat:message', (data) => {9 // data is already parsed from JSON by Socket.IO10 const enriched = {11 ...data,12 id: crypto.randomUUID(),13 timestamp: new Date().toISOString(),14 senderId: socket.id,15 };16 io.to(data.roomId).emit('chat:message', enriched);17 });1819 socket.on('room:join', (data) => {20 socket.join(data.roomId);21 socket.to(data.roomId).emit('room:userJoined', {22 userId: socket.id,23 roomId: data.roomId,24 });25 });26});Socket.IO clienttypescript
1import { io } from 'socket.io-client';23const socket = io('http://localhost:3001');45socket.emit('room:join', { roomId: 'room_1' });67socket.emit('chat:message', {8 text: 'Hello everyone!',9 roomId: 'room_1',10});1112socket.on('chat:message', (data) => {13 // data is already a JavaScript object — Socket.IO handles JSON14 console.log(`${data.senderId}: ${data.text}`);15});Connection Lifecycle Management
Reconnection with exponential backofftypescript
1function createReconnectingSocket(url: string) {2 let ws: WebSocket;3 let retries = 0;4 const maxRetries = 10;5 const messageQueue: string[] = [];67 function connect() {8 ws = new WebSocket(url);910 ws.onopen = () => {11 retries = 0;12 // Flush queued messages13 while (messageQueue.length > 0) {14 ws.send(messageQueue.shift()!);15 }16 };1718 ws.onclose = () => {19 if (retries < maxRetries) {20 const delay = Math.min(1000 * 2 ** retries, 30000);21 retries++;22 setTimeout(connect, delay);23 }24 };2526 ws.onmessage = (event) => {27 const message = JSON.parse(event.data);28 handleMessage(message);29 };30 }3132 function send(type: string, payload: unknown) {33 const json = JSON.stringify({ type, payload, timestamp: new Date().toISOString() });34 if (ws.readyState === WebSocket.OPEN) {35 ws.send(json);36 } else {37 messageQueue.push(json);38 }39 }4041 connect();42 return { send };43}Best Practices
- ✓Always use a message envelope with
type,payload, andtimestamp - ✓Wrap
JSON.parse()in try/catch — clients can send anything - ✓Validate messages with JSON Schema or Zod before processing
- ✓Implement heartbeat/ping every 30 seconds to detect stale connections
- ✓Add message IDs for deduplication (clients may resend on reconnect)
- ✓Queue outgoing messages during disconnection, replay after reconnect
- ✗Don't send sensitive data (passwords, tokens) in plaintext WebSocket messages
- ✗Don't trust client-sent timestamps — always add a server timestamp
Try It — Validate a WebSocket Message
Try It Yourself
A valid WebSocket JSON message envelope
Try These Tools
Continue Learning
Frequently Asked Questions
What is the best format for WebSocket messages?
JSON is the most common format for WebSocket messages because it is human-readable and supported everywhere. Wrap each message in a standard envelope: {"type": "event_name", "payload": {...}, "timestamp": "..."}. For high-throughput scenarios, consider MessagePack or Protocol Buffers.
What is the difference between WebSockets and Server-Sent Events?
WebSockets provide full-duplex (two-way) communication — both client and server can send messages. SSE is one-way (server to client only). SSE uses regular HTTP, supports automatic reconnection, and is simpler to implement. Use WebSockets for chat, games, and collaborative editing. Use SSE for dashboards, notifications, and live feeds.
Does Socket.IO use JSON?
Yes, by default. Socket.IO serializes event data as JSON automatically. When you emit socket.emit("message", {text: "hello"}), Socket.IO wraps it in its own protocol format with the JSON payload. Custom parsers (like socket.io-msgpack) can replace JSON for performance.
How should I handle connection errors in WebSockets?
Implement automatic reconnection with exponential backoff. Send a JSON heartbeat/ping every 30 seconds to detect stale connections. Queue messages during disconnection and replay them after reconnecting. Always validate incoming JSON with try/catch around JSON.parse().
Can I use JSON Schema to validate WebSocket messages?
Yes, and you should. Define a schema for each message type and validate incoming messages server-side before processing. Libraries like Ajv work with both HTTP and WebSocket handlers. This prevents malformed messages from crashing your application.