Real-Time Dashboards with Server-Sent Events and Next.js 15
Ahmed Anis
CTO
14 November 2025
7 min readEvery time a product team asks us to add real-time updates to their dashboard, the first suggestion is WebSockets. It's the intuitive choice — bidirectional, low-latency, widely supported. But for dashboards that stream server-to-client data (metrics, logs, live activity feeds), Server-Sent Events are almost always the better choice. They're simpler, cheaper to operate, and work seamlessly with Next.js's streaming model.
When SSE Beats WebSockets
- One-directional streams: If the client only reads and never sends, SSE is purpose-built for this. No bidirectional handshake overhead, no upgrade negotiation.
- HTTP/2 multiplexing: SSE runs over standard HTTP/2, which means multiple event streams share a single connection. WebSockets require their own persistent TCP connection per stream.
- Automatic reconnect: The browser EventSource API handles reconnection automatically with the Last-Event-ID header. WebSocket reconnection logic is your responsibility.
- CDN and proxy compatibility: SSE works transparently through Vercel, Cloudflare, and any standard proxy. WebSockets need explicit pass-through configuration.
The Next.js Route Handler Pattern
// app/api/metrics/stream/route.ts
import { metricsEmitter } from '@/lib/metrics'
export async function GET() {
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder()
const send = (data: object) => {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
)
}
metricsEmitter.on('update', send)
// Heartbeat every 30s to keep connection alive
const heartbeat = setInterval(() => {
controller.enqueue(encoder.encode(': keep-alive\n\n'))
}, 30_000)
return () => {
metricsEmitter.off('update', send)
clearInterval(heartbeat)
}
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
}💡 Buffer events during reconnection
Store the last N events server-side keyed by event ID. When a client reconnects with Last-Event-ID, replay missed events before resuming the live stream. This prevents data gaps during transient network issues.
React Hook for SSE Consumption
// hooks/useEventStream.ts
import { useEffect, useState } from 'react'
export function useEventStream<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
const source = new EventSource(url)
source.onmessage = (e) => {
try {
setData(JSON.parse(e.data) as T)
} catch {
setError(new Error('Failed to parse event data'))
}
}
source.onerror = () => {
setError(new Error('SSE connection failed'))
}
return () => source.close()
}, [url])
return { data, error }
}20+
Real-time dashboards built
3×
Lower server overhead vs WebSockets
99.8%
Avg SSE connection reliability
<50ms
Event delivery latency (p50)
Tags
You might also like
Work with us
Ready to build your product?
We help product teams across the UK, Netherlands, Australia, and North America ship faster without compromising quality. Let's talk about your project.
Talk to our team →
