Learn/Advanced Topics

Real-time JSON — WebSockets, SSE & Socket.IO

REST APIs send JSON on request. But what about live data — chat messages, stock tickers, notifications, collaborative editing? This guide covers how to send and receive JSON in real time using WebSockets, Server-Sent Events, and Socket.IO.

Choosing a Real-time Transport

Real-time Transport Decision Tree
FeatureWebSocketServer-Sent EventsSocket.IO
DirectionFull-duplex (both ways)Server → Client onlyFull-duplex (both ways)
Protocolws:// / wss://HTTP (text/event-stream)WebSocket + HTTP fallback
ReconnectionManualAutomatic (built-in)Automatic
JSON supportManual serializationText data (parse manually)Built-in serialization
Rooms / ChannelsManualNot built-inBuilt-in rooms
Browser supportAll modern browsersAll except IEAll (with fallback)
Best forChat, games, collabDashboards, feeds, logsComplex 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 data
4 id?: string; // Message ID for deduplication
5 timestamp: string; // ISO 8601
6}
7
8// Examples
9const 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};
15
16const typingEvent: WebSocketMessage = {
17 type: "user.typing",
18 payload: { userId: "user_42", roomId: "room_1" },
19 timestamp: "2026-04-02T14:30:05Z",
20};
21
22const 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');
2
3ws.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});
11
12ws.addEventListener('message', (event) => {
13 try {
14 const message = JSON.parse(event.data);
15
16 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});
33
34ws.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';
2
3const wss = new WebSocketServer({ port: 8080 });
4
5wss.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 }
18
19 if (message.type === 'chat.message') {
20 // Broadcast to all connected clients
21 const broadcast = JSON.stringify({
22 type: 'chat.message',
23 payload: message.payload,
24 id: crypto.randomUUID(),
25 timestamp: new Date().toISOString(),
26 });
27
28 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();
3
4app.get('/events', (req, res) => {
5 res.writeHead(200, {
6 'Content-Type': 'text/event-stream',
7 'Cache-Control': 'no-cache',
8 'Connection': 'keep-alive',
9 });
10
11 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 };
15
16 // Send initial data
17 sendEvent('connected', { message: 'Stream started' });
18
19 // Send periodic updates
20 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);
27
28 req.on('close', () => clearInterval(interval));
29});

Client (Browser)

EventSource clienttypescript
1const source = new EventSource('/events');
2
3source.addEventListener('stock.update', (event) => {
4 const data = JSON.parse(event.data);
5 updateStockTicker(data.payload);
6});
7
8source.addEventListener('error', () => {
9 console.log('SSE connection lost — will auto-reconnect');
10});
11
12// To close manually
13// source.close();

3. Socket.IO with JSON

Socket.IO servertypescript
1import { Server } from 'socket.io';
2
3const io = new Server(3001, { cors: { origin: '*' } });
4
5io.on('connection', (socket) => {
6 console.log('Client connected:', socket.id);
7
8 socket.on('chat:message', (data) => {
9 // data is already parsed from JSON by Socket.IO
10 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 });
18
19 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';
2
3const socket = io('http://localhost:3001');
4
5socket.emit('room:join', { roomId: 'room_1' });
6
7socket.emit('chat:message', {
8 text: 'Hello everyone!',
9 roomId: 'room_1',
10});
11
12socket.on('chat:message', (data) => {
13 // data is already a JavaScript object — Socket.IO handles JSON
14 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[] = [];
6
7 function connect() {
8 ws = new WebSocket(url);
9
10 ws.onopen = () => {
11 retries = 0;
12 // Flush queued messages
13 while (messageQueue.length > 0) {
14 ws.send(messageQueue.shift()!);
15 }
16 };
17
18 ws.onclose = () => {
19 if (retries < maxRetries) {
20 const delay = Math.min(1000 * 2 ** retries, 30000);
21 retries++;
22 setTimeout(connect, delay);
23 }
24 };
25
26 ws.onmessage = (event) => {
27 const message = JSON.parse(event.data);
28 handleMessage(message);
29 };
30 }
31
32 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 }
40
41 connect();
42 return { send };
43}

Best Practices

  • Always use a message envelope with type, payload, and timestamp
  • 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

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.