Real-Time Updates with Server-Sent Events (SSE) in Next.js 15
Stream multiple named events from a Next.js 15 endpoint and consume them via the useEventSource hook from react-use-websocket.
Server-Sent Events (SSE) let you push a continuous stream of updates from your server to the browser over a plain HTTP connection—no WebSocket handshake, no bidirectional overhead. In this tutorial you'll build a multi-event SSE endpoint in Next.js 15 (App Router) that emits two named event types—news and stats—and consume them in a React client component using the useEventSource hook from react-use-websocket.
By the end you'll understand how SSE named events work, where the common pitfalls are (memory leaks, reconnection, unbounded state), and how to extend the pattern for production use.
1. Project Setup
Create a new Next.js 15 project with the App Router, a src directory, and TypeScript:
This creates a folder structure like:
Then install react-use-websocket:
Version note:
react-use-websocketv4.x requires React 18. If you're on React 17, pin to[email protected]instead.
2. Creating an SSE Endpoint
We'll create a streaming route.ts that periodically sends two different named events:
news— Simulated "breaking news" headlines.stats— Random numeric data such as "active users" or "sales figures."
Create the file at src/app/api/stream/route.ts:
Key points
| Concept | Detail |
|---|---|
| Named events | event: news / event: stats before each data: line tells the browser which listener to fire. Without an event: line the browser fires the generic message event. |
| Double newline | Each SSE message must end with \n\n. A single \n separates fields within one message. |
| JSON payloads | Each event type carries its own data shape—a headline string for news, and activeUsers/sales numbers for stats. |
| Cleanup | Returning a function from start() ensures the interval is cleared when the client disconnects, preventing a memory/CPU leak. |
Cache-Control: no-cache | Required to prevent intermediate proxies (nginx, CDN) from buffering the stream. |
Visit http://localhost:3000/api/stream while the dev server is running and you'll see the raw event stream in your browser.
3. Consuming SSE with useEventSource
In Next.js App Router, layouts and pages are Server Components by default. You need to opt into a Client Component (via the "use client" directive) to use browser APIs such as EventSource. The useEventSource hook from react-use-websocket wraps the native EventSource API and adds a React-friendly interface with named-event routing.
Create or edit src/app/page.tsx:
How useEventSource works
useEventSource(endpoint, options)opens a nativeEventSourceconnection toendpoint.options.eventsis a map of named event types → callback functions. When the server sendsevent: news, thenewscallback fires with the rawMessageEvent;event: statsfiresstats, and so on.readyStatemirrors the nativeEventSourceready states:0(CONNECTING),1(OPEN),2(CLOSED).- The hook automatically closes the connection when the component unmounts, so you don't need a manual
useEffectcleanup.
Bounded state — why it matters
The two .slice(0, MAX_ITEMS) calls keep the arrays capped at 50 entries each. Without a cap, arrays grow indefinitely for as long as the connection is open: in a dashboard that runs for hours, this silently consumes memory and degrades React reconciliation performance.
4. Reconnection and Error Handling
The native EventSource API reconnects automatically after a dropped connection (the browser waits ~3 seconds by default). However there are scenarios you should handle explicitly:
Server-side: signal the retry interval
Add a retry: field to your event stream to tell the browser how long to wait before reconnecting (milliseconds):
Server-side: send a heartbeat to keep connections alive
Proxies and load balancers often close idle connections after 30–60 seconds. A periodic comment line keeps the connection alive without triggering event handlers:
Client-side: detect and display a closed connection
Authentication: pass credentials via query params or cookies
The native EventSource only supports GET requests and cannot set custom headers. The two common patterns are:
- Cookie-based auth — set
withCredentials: truein the options; the browser sends cookies automatically. - Query-string token — append a short-lived token:
useEventSource("/api/stream?token=xyz", …).
5. Trying It Out
- Start the dev server:
- Visit http://localhost:3000.
- Every 3 seconds you'll see:
- One "news" event prepended to "Latest Headlines."
- One "stats" event prepended to "Stats Log."
Open the Network tab in DevTools → select the /api/stream request → click the EventStream sub-tab to watch raw SSE frames arrive in real time.
6. Expanding the Pattern
| Idea | How |
|---|---|
| Different frequencies | Use separate setInterval calls per event type, or a single interval with a counter to skip every N ticks. |
| More event types | Add alerts, notifications, or chat keys to both the server event: lines and the client events map. |
| Real data sources | Replace the random generators with database queries, message-queue consumers (Redis Pub/Sub, Kafka), or third-party API calls. |
Last-Event-ID resumption | Set id: <value> on each server message; the browser sends Last-Event-ID on reconnect so you can replay missed events. |
| Vercel / edge deployment | Switch the route to the Edge Runtime (export const runtime = "edge") for lower cold-start latency; note that the Edge Runtime has a 30-second response limit unless you use streaming correctly. |
7. SSE vs. WebSockets — Quick Reference
| Feature | SSE | WebSocket |
|---|---|---|
| Direction | Server → Client only | Full-duplex |
| Protocol | Plain HTTP/1.1 or HTTP/2 | Upgraded connection (ws://) |
| Auto-reconnect | Built into the browser | Must implement manually |
| Named event types | Native (event: field) | Custom (message envelope) |
| Proxy/firewall friendliness | High (standard HTTP) | Lower (non-standard upgrade) |
| Max concurrent connections | 6 per origin (HTTP/1.1) | Unlimited |
Choose SSE when you only need server-to-client streaming and want simplicity; choose WebSockets when clients also need to send frequent messages back to the server.
8. Conclusion
By combining SSE with Next.js 15's App Router and the useEventSource hook from react-use-websocket, you can:
- Push real-time data to connected clients with minimal overhead—no handshake complexity.
- Route named events (
news,stats, etc.) directly to typed callbacks, keeping component logic clean. - Handle edge cases safely—bounded state, heartbeats, retry intervals, and auth patterns all covered above.
Key takeaways:
- Endpoint: a streaming
route.tsthat writesevent: <name>\ndata: …\n\nchunks. - Client: a
"use client"component withuseEventSource, which routes named events to callbacks. - Production hygiene: cap array state, send heartbeats, set
retry:, and handlereadyState === 2. - Versatility: perfect for live dashboards, notification feeds, leaderboards, or any scenario needing continuous one-way server-to-client updates.
Damian Hodgkiss
Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.