Building Real-Time Features with WebSockets and Server-Sent Events

Real-time features are no longer optional — users expect live updates, instant notifications, and collaborative experiences. WebSockets and Server-Sent Events (SSE) are the two primary technologies for building them. Each has strengths and trade-offs that determine the right use case.
WebSockets: Full-Duplex Communication
WebSockets maintain a persistent TCP connection between client and server, allowing bidirectional data flow. Use them when the client needs to send data to the server as frequently as it receives updates — chat applications, collaborative editing, and multiplayer games.
// Server-side WebSocket with Node.js
import { WebSocketServer } from 'ws'
const wss = new WebSocketServer({ port: 8080 })
wss.on('connection', (ws, req) => {
const userId = authenticateConnection(req.url)
if (!userId) {
ws.close(4001, 'Unauthorized')
return
}
ws.on('message', (data) => {
const message = JSON.parse(data.toString())
broadcastToRoom(message.roomId, {
userId,
content: message.content,
timestamp: Date.now(),
})
})
ws.on('close', () => {
handleDisconnect(userId)
})
})
// Client-side WebSocket
const ws = new WebSocket('wss://api.example.com/ws?token=jwt-token')
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
// Update UI with the new data
addMessageToChat(update)
}
ws.send(JSON.stringify({ type: 'message', roomId: 'general', content: 'Hello' }))
Connection management is critical with WebSockets. Implement heartbeat ping/pong intervals to detect stale connections, and reconnection logic with exponential backoff on the client side.
Server-Sent Events: Simpler Server-to-Client Streaming
SSE opens a single HTTP connection from the client to the server, and the server pushes events over that connection. It is unidirectional — the client cannot send data over the same connection. SSE is significantly simpler to implement than WebSockets and works over HTTP/1.1 and HTTP/2.
// Next.js Route Handler using SSE
export async function GET(request: NextRequest) {
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder()
// Send initial connection event
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: 'connected' })}\n\n`)
)
// Push updates every 5 seconds
const interval = setInterval(async () => {
const updates = await fetchNewUpdates()
if (updates.length > 0) {
for (const update of updates) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(update)}\n\n`)
)
}
}
}, 5000)
// Cleanup on disconnect
request.signal.addEventListener('abort', () => {
clearInterval(interval)
controller.close()
})
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
}
// Client-side SSE
const eventSource = new EventSource('/api/updates')
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
updateDashboard(data)
}
eventSource.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data)
showToast(notification)
})
SSE automatically reconnects when the connection drops — no manual reconnection logic needed. It is the right choice for live dashboards, stock tickers, notification feeds, and activity streams.
Choosing Between WebSockets and SSE
Use Case | WebSocket | SSE
-----------------------------|------------|----------------
Chat / messaging | ✅ Best | ❌ No client send
Live dashboard | Possible | ✅ Best
Real-time notifications | Overkill | ✅ Best
Collaborative editing | ✅ Best | ❌ No client send
File upload progress | Possible | ✅ Best
Gaming / low-latency | ✅ Best | ❌ Too much overhead
Stock ticker / price feeds | Possible | ✅ Best
The decision comes down to whether your client needs to send data. If it does not, SSE is almost always the simpler, more reliable choice. If bidirectional communication is required, WebSockets are the answer.
Scaling Real-Time Connections
Both technologies face scaling challenges. A single server handles only so many concurrent connections. For production deployments:
- Use a message broker (Redis Pub/Sub, RabbitMQ) to broadcast events across multiple server instances
- Front connections with a load balancer that supports sticky sessions or route via a WebSocket-aware proxy
- Consider managed services — Pusher for WebSockets, or serverless SSE with Cloudflare Durable Objects
// Scaling with Redis Pub/Sub
import { createClient } from 'redis'
const pub = createClient()
const sub = createClient()
sub.subscribe('notifications', (message) => {
// Broadcast to all connected WebSocket clients on this server
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message)
}
})
})
Each server instance subscribes to the same channels. Any server that publishes an event sends it through Redis, and every other server receives and broadcasts it to its local connections.
Building real-time features requires careful architecture decisions around protocol choice, connection management, and horizontal scaling. At SoniNow, we design and implement real-time systems that handle thousands of concurrent connections with reliable delivery and low latency.
Ready to add real-time features to your application? Talk to SoniNow about your use case and let us architect the right solution.
Related Insights

API Rate Limiting Strategies: Token Bucket, Leaky Bucket, and Sliding Window
A guide to implementing API rate limiting including token bucket, leaky bucket, sliding window, and distributed rate limiting with Redis for production APIs.

Authentication Patterns in Modern Web Apps: JWT, Sessions, and Passkeys
A guide to modern authentication patterns comparing JWT, session-based auth, and passkeys including implementation strategies, security considerations, and user experience.

Code Splitting and Lazy Loading in React: Performance Optimization Guide
A comprehensive guide to code splitting and lazy loading in React applications including React.lazy, Suspense, route-based splitting, and component-level chunking.