WebSockets and Real-Time Apps 2026: Socket.io, SSE, and Pusher Guide
Advertisement
Real-Time in 2026: Pick the Right Protocol
| Protocol | Use Case | Connection | Server Load |
|---|---|---|---|
| WebSocket | Chat, games, collaboration | Persistent | High |
| SSE | Live feeds, dashboards | Persistent (one-way) | Medium |
| Long Polling | Simple notifications | Polling | Low |
| Pusher/Ably | Serverless real-time | Managed | None |
- WebSocket Chat with Socket.io
- React Client with Socket.io
- Server-Sent Events (SSE): Live Dashboards
- Scale with Redis Adapter
WebSocket Chat with Socket.io
npm install socket.io socket.io-client
// server.ts — Socket.io server
import { createServer } from 'http'
import { Server } from 'socket.io'
import express from 'express'
const app = express()
const httpServer = createServer(app)
const io = new Server(httpServer, {
cors: { origin: process.env.CLIENT_URL, credentials: true },
transports: ['websocket', 'polling'],
})
// Types
interface ChatMessage {
id: string
roomId: string
userId: string
username: string
content: string
timestamp: number
}
// Middleware: authenticate socket connection
io.use(async (socket, next) => {
const token = socket.handshake.auth.token
try {
const user = verifyToken(token)
socket.data.user = user
next()
} catch {
next(new Error('Unauthorized'))
}
})
io.on('connection', (socket) => {
const user = socket.data.user
console.log(`${user.name} connected`)
// Join a room
socket.on('join_room', async (roomId: string) => {
socket.join(roomId)
// Load recent messages
const messages = await getRecentMessages(roomId, 50)
socket.emit('message_history', messages)
// Notify room of new user
socket.to(roomId).emit('user_joined', { userId: user.id, username: user.name })
// Track presence
const members = await getActiveMembers(roomId)
io.to(roomId).emit('presence_update', members)
})
socket.on('send_message', async (data: { roomId: string; content: string }) => {
const message: ChatMessage = {
id: crypto.randomUUID(),
roomId: data.roomId,
userId: user.id,
username: user.name,
content: data.content,
timestamp: Date.now(),
}
// Save to DB
await saveMessage(message)
// Broadcast to all in room (including sender)
io.to(data.roomId).emit('new_message', message)
})
socket.on('typing', (roomId: string) => {
socket.to(roomId).emit('user_typing', { userId: user.id, username: user.name })
})
socket.on('disconnect', () => {
console.log(`${user.name} disconnected`)
// Update presence in all rooms
})
})
httpServer.listen(3001)
React Client with Socket.io
// hooks/use-chat.ts
'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
import { io, Socket } from 'socket.io-client'
interface Message {
id: string
userId: string
username: string
content: string
timestamp: number
}
export function useChat(roomId: string) {
const socketRef = useRef<Socket | null>(null)
const [messages, setMessages] = useState<Message[]>([])
const [isConnected, setIsConnected] = useState(false)
const [typingUsers, setTypingUsers] = useState<string[]>([])
useEffect(() => {
const token = getAuthToken()
const socket = io(process.env.NEXT_PUBLIC_SOCKET_URL!, {
auth: { token },
transports: ['websocket'],
})
socketRef.current = socket
socket.on('connect', () => setIsConnected(true))
socket.on('disconnect', () => setIsConnected(false))
socket.on('message_history', (history: Message[]) => {
setMessages(history)
})
socket.on('new_message', (message: Message) => {
setMessages(prev => [...prev, message])
})
socket.on('user_typing', ({ username }: { username: string }) => {
setTypingUsers(prev => [...new Set([...prev, username])])
setTimeout(() => {
setTypingUsers(prev => prev.filter(u => u !== username))
}, 3000)
})
socket.emit('join_room', roomId)
return () => {
socket.disconnect()
}
}, [roomId])
const sendMessage = useCallback((content: string) => {
socketRef.current?.emit('send_message', { roomId, content })
}, [roomId])
const notifyTyping = useCallback(() => {
socketRef.current?.emit('typing', roomId)
}, [roomId])
return { messages, isConnected, typingUsers, sendMessage, notifyTyping }
}
// Chat UI
export function ChatRoom({ roomId }: { roomId: string }) {
const { messages, isConnected, typingUsers, sendMessage, notifyTyping } = useChat(roomId)
const [input, setInput] = useState('')
const messagesEndRef = useRef<HTMLDivElement>(null)
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{messages.map(msg => (
<div key={msg.id} className="flex gap-2">
<span className="font-bold text-blue-600">{msg.username}</span>
<span>{msg.content}</span>
</div>
))}
{typingUsers.length > 0 && (
<div className="text-gray-500 text-sm italic">
{typingUsers.join(', ')} {typingUsers.length === 1 ? 'is' : 'are'} typing...
</div>
)}
<div ref={messagesEndRef} />
</div>
<form
onSubmit={(e) => { e.preventDefault(); if (input.trim()) { sendMessage(input); setInput('') } }}
className="p-4 flex gap-2"
>
<input
value={input}
onChange={(e) => { setInput(e.target.value); notifyTyping() }}
placeholder="Type a message..."
className="flex-1 border rounded px-3 py-2"
/>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">Send</button>
</form>
</div>
)
}
Server-Sent Events (SSE): Live Dashboards
SSE is perfect for one-way streams (server to client) — no overhead of full WebSocket:
// app/api/events/route.ts — Next.js SSE endpoint
export async function GET(request: Request) {
const encoder = new TextEncoder()
const stream = new ReadableStream({
start(controller) {
// Send initial data
const send = (data: object) => {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
)
}
send({ type: 'connected', timestamp: Date.now() })
// Interval updates
const interval = setInterval(async () => {
const stats = await getRealtimeStats()
send({ type: 'stats', data: stats })
}, 1000)
// Subscribe to Redis events
const redisSubscriber = redis.duplicate()
redisSubscriber.subscribe('dashboard:updates')
redisSubscriber.on('message', (_, message) => {
send({ type: 'update', data: JSON.parse(message) })
})
// Cleanup on disconnect
request.signal.addEventListener('abort', () => {
clearInterval(interval)
redisSubscriber.unsubscribe()
redisSubscriber.quit()
controller.close()
})
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
}
// React hook for SSE
function useLiveDashboard() {
const [stats, setStats] = useState<Stats | null>(null)
useEffect(() => {
const eventSource = new EventSource('/api/events')
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'stats') setStats(data.data)
}
eventSource.onerror = () => eventSource.close()
return () => eventSource.close()
}, [])
return stats
}
Scale with Redis Adapter
// Multiple Node.js instances need Redis to share Socket.io state
import { createAdapter } from '@socket.io/redis-adapter'
import Redis from 'ioredis'
const pubClient = new Redis(process.env.REDIS_URL!)
const subClient = pubClient.duplicate()
io.adapter(createAdapter(pubClient, subClient))
// Now messages broadcast across all Node.js instances
// io.to(roomId).emit('message', data) → works across servers
Real-time features are table stakes for modern apps. Pick WebSocket for bidirectional communication, SSE for feeds and dashboards, Pusher for serverless.
Advertisement