WebSockets and Real-Time Apps 2026 — Socket.io, SSE, and Pusher Guide
Advertisement
Introduction
Why This Matters
Polling for updates wastes bandwidth and adds latency. Real-time connections — WebSockets for bidirectional, SSE for server-to-client — are the standard for chat, notifications, live scores, and collaborative features in 2026. Choosing the right technology depends on the communication direction and infrastructure constraints.
Choosing the Right Technology
| Use Case | Best Choice | Reason |
|---|---|---|
| Chat / multiplayer | WebSocket (Socket.io) | Bidirectional, low latency |
| Notifications / live feed | Server-Sent Events (SSE) | Simple, HTTP/2, auto-reconnect |
| Managed at scale | Pusher / Ably | No infrastructure to manage |
| Real-time dashboard | SSE or WebSocket | Depends on user interaction |
Socket.io Server
npm install socket.io
npm install -D @types/socket.io// src/socket/server.ts
import { Server } from 'socket.io'
import { createServer } from 'http'
import { app } from '../server' // Fastify or Express instance
import { verifyToken } from '../lib/auth'
const httpServer = createServer(app.server ?? app)
const io = new Server(httpServer, {
cors: { origin: process.env.CLIENT_URL, credentials: true },
transports: ['websocket', 'polling'],
})
// Authentication middleware
io.use(async (socket, next) => {
const token = socket.handshake.auth.token
if (!token) return next(new Error('Authentication required'))
const user = await verifyToken(token)
if (!user) return next(new Error('Invalid token'))
socket.data.user = user
next()
})
// Room-based chat
io.on('connection', (socket) => {
const userId = socket.data.user.id
console.log(`User ${userId} connected`)
socket.on('join:room', (roomId: string) => {
socket.join(roomId)
socket.to(roomId).emit('user:joined', { userId, roomId })
})
socket.on('message:send', async ({ roomId, content }: { roomId: string; content: string }) => {
const message = await saveMessage({ roomId, userId, content })
// Broadcast to everyone in the room including sender
io.to(roomId).emit('message:new', message)
})
socket.on('typing:start', ({ roomId }: { roomId: string }) => {
socket.to(roomId).emit('typing:update', { userId, isTyping: true })
})
socket.on('typing:stop', ({ roomId }: { roomId: string }) => {
socket.to(roomId).emit('typing:update', { userId, isTyping: false })
})
socket.on('disconnect', (reason) => {
console.log(`User ${userId} disconnected:`, reason)
// Notify rooms the user was in
socket.rooms.forEach(roomId => {
io.to(roomId).emit('user:left', { userId })
})
})
})
httpServer.listen(4000)Socket.io React Client
// src/hooks/useSocket.ts
import { useEffect, useRef, useCallback } from 'react'
import { io, Socket } from 'socket.io-client'
export function useSocket(token: string) {
const socketRef = useRef<Socket | null>(null)
useEffect(() => {
socketRef.current = io(process.env.NEXT_PUBLIC_SOCKET_URL!, {
auth: { token },
transports: ['websocket'],
})
return () => {
socketRef.current?.disconnect()
}
}, [token])
return socketRef
}
// src/components/ChatRoom.tsx
'use client'
import { useState, useEffect, useRef } from 'react'
import { useSocket } from '@/hooks/useSocket'
export function ChatRoom({ roomId, token }: { roomId: string; token: string }) {
const [messages, setMessages] = useState<Message[]>([])
const [typingUsers, setTypingUsers] = useState<string[]>([])
const socketRef = useSocket(token)
const bottomRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const socket = socketRef.current
if (!socket) return
socket.emit('join:room', roomId)
socket.on('message:new', (msg: Message) => {
setMessages(prev => [...prev, msg])
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
})
socket.on('typing:update', ({ userId, isTyping }: { userId: string; isTyping: boolean }) => {
setTypingUsers(prev =>
isTyping ? [...prev, userId] : prev.filter(id => id !== userId)
)
})
return () => {
socket.off('message:new')
socket.off('typing:update')
}
}, [roomId, socketRef])
function sendMessage(content: string) {
socketRef.current?.emit('message:send', { roomId, content })
}
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{messages.map(msg => <MessageBubble key={msg.id} message={msg} />)}
{typingUsers.length > 0 && (
<p className="text-gray-500 text-sm">{typingUsers.join(', ')} is typing…</p>
)}
<div ref={bottomRef} />
</div>
<MessageInput onSend={sendMessage} socket={socketRef.current} roomId={roomId} />
</div>
)
}Server-Sent Events (SSE)
SSE is simpler than WebSockets for one-way server-to-client streams:
// app/api/notifications/stream/route.ts (Next.js)
import { auth } from '@/auth'
export async function GET() {
const session = await auth()
if (!session) return new Response('Unauthorized', { status: 401 })
const userId = session.user.id
const encoder = new TextEncoder()
const stream = new ReadableStream({
start(controller) {
function sendEvent(event: string, data: unknown) {
controller.enqueue(
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
)
}
sendEvent('connected', { message: 'Stream established' })
// Subscribe to Redis pub/sub for this user
const sub = subscribeToUserEvents(userId, (event) => {
sendEvent(event.type, event.data)
})
// Heartbeat to keep connection alive
const heartbeat = setInterval(() => {
sendEvent('ping', { ts: Date.now() })
}, 30_000)
return () => {
clearInterval(heartbeat)
sub.unsubscribe()
}
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
}// React SSE client
'use client'
import { useEffect, useState } from 'react'
export function useNotifications() {
const [notifications, setNotifications] = useState<Notification[]>([])
useEffect(() => {
const es = new EventSource('/api/notifications/stream')
es.addEventListener('notification', (e) => {
setNotifications(prev => [JSON.parse(e.data), ...prev])
})
es.onerror = () => {
console.warn('SSE disconnected, will auto-reconnect')
}
return () => es.close()
}, [])
return notifications
}Common Mistakes
- Not authenticating WebSocket connections — the
upgraderequest can be forged - Keeping state in memory on a single server — breaks when scaled to multiple instances; use Redis pub/sub
- Forgetting to handle reconnection — clients must reconnect after network drops
- Not implementing backpressure — a slow client can crash the server with unbuffered events
- Using WebSockets when SSE suffices — SSE works over HTTP/2, is simpler, and auto-reconnects
Best Practices
- Use Socket.io rooms to namespace broadcasts — avoid broadcasting to all connected sockets
- Use Redis adapter (
@socket.io/redis-adapter) when running multiple server instances - Send a heartbeat ping every 30 seconds to detect dead connections
- Use SSE for notification feeds and live dashboards — reserve WebSockets for true bidirectional use cases
- Authenticate at the connection level, not per-message — verify JWT in the Socket.io middleware hook
Key Takeaways
- WebSockets enable bidirectional communication — ideal for chat, multiplayer, and collaborative editing
- Server-Sent Events are unidirectional (server to client), simpler, and auto-reconnect natively
- Socket.io middleware runs before the
connectionevent — authenticate tokens there - Redis pub/sub (
@socket.io/redis-adapter) is required to broadcast across multiple server instances - SSE over HTTP/2 multiplexes streams without extra connections — no WebSocket upgrade needed
- Heartbeat pings every 30 seconds keep connections alive through proxies and load balancers
- Pusher and Ably are managed WebSocket services that eliminate infrastructure complexity at a cost
- Typing indicators should use debouncing and
typing:stopevents to reduce noise
Advertisement
Related reading
WebSockets with Node.js — Real-Time Apps Guide 20266 min readServer-Sent Events (SSE) — Real-Time Streaming Guide 20266 min readServer-Sent Events in Production — Simpler Than WebSockets for Most Use Cases7 min readWebSockets at Scale in 2026 — Beyond Socket.io to Production-Grade Real-Time8 min readWebSockets vs SSE vs Long Polling — Choosing Real-Time Communication in 20269 min readBuild an AI Chatbot with Next.js 15 and OpenAI — Full Stack 20266 min read