Real-time data streaming is essential for modern web applications, powering features like low-latency audio/visual streaming, stock updates, collaborative tools, and live geolocation. Next.js provides robust support for implementing both WebSockets and Server-Sent Events (SSE), making it an excellent choice for building scalable real-time solutions. In this guide, we’ll explore these technologies, compare their strengths and weaknesses, and outline practical implementation strategies for integrating them into your Next.js applications.
Before diving into implementations, let’s clarify the key differences between WebSockets and SSE:
WebSockets are a computer communications protocol that enable real-time, bidirectional communication between a client and a server over a single Transmission Control Protocol (TCP) connection.
Server-Sent Events (SSE) is a unidirectional communication protocol that allows servers to push real-time updates to clients over a single HTTP connection. Unlike WebSockets, SSE is designed for scenarios where the server continuously sends data without expecting responses from the client.
Let’s explore how to implement both approaches in a Next.js 15 application.
Next.Js API routes and Route handlers are for serverless functions which mean they do not support websocket servers.
For this guide, we’ll implement a simple WebSocket server that emits messages to connected clients. If you don't have one, you can quickly create a server using Node.js on your local machine as shown below:
const express = require("express");
const http = require("http");
const WebSocket = require("ws");
const app = express();
// Create an HTTP server
const server = http.createServer(app);
// Create a WebSocket server
const wss = new WebSocket.Server({ server, path: "/ws" });
// WebSocket connection handling
wss.on("connection", (ws) => {
console.log("New WebSocket connection");
// Send a welcome message to the client
ws.send(
JSON.stringify({ type: "welcome", message: "Connected to WebSocket API!" })
);
// Handle messages from the client
ws.on("message", (message) => {
console.log("Received:", message);
// Broadcast the message to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: "broadcast", data: message }));
}
});
});
// Handle disconnection
ws.on("close", () => {
console.log("WebSocket connection closed");
});
});
// Start the HTTP server
const PORT = 3000;
server.listen(PORT, () => {
console.log(`API server running at http://localhost:${PORT}`);
console.log(`WebSocket endpoint available at ws://localhost:${PORT}/ws`);
});
Now create a hook in your Next.js codebase called useWebsocket.ts
import { useEffect, useRef, useState } from "react";
interface UseWebSocketOptions {
onOpen?: (event: Event) => void;
onMessage?: (event: MessageEvent) => void;
onClose?: (event: CloseEvent) => void;
onError?: (event: Event) => void;
reconnectAttempts?: number;
reconnectInterval?: number;
}
export const useWebSocket = (
url: string,
options: UseWebSocketOptions = {}
) => {
const {
onOpen,
onMessage,
onClose,
onError,
reconnectAttempts = 5,
reconnectInterval = 3000,
} = options;
const [isConnected, setIsConnected] = useState(false);
const [isReconnecting, setIsReconnecting] = useState(false);
const webSocketRef = useRef<WebSocket | null>(null);
const attemptsRef = useRef(0);
const connectWebSocket = () => {
setIsReconnecting(false);
attemptsRef.current = 0;
const ws = new WebSocket(url);
webSocketRef.current = ws;
ws.onopen = (event) => {
setIsConnected(true);
setIsReconnecting(false);
if (onOpen) onOpen(event);
};
ws.onmessage = (event) => {
if (onMessage) onMessage(event);
};
ws.onclose = (event) => {
setIsConnected(false);
if (onClose) onClose(event);
// Attempt reconnection if allowed
if (attemptsRef.current < reconnectAttempts) {
setIsReconnecting(true);
attemptsRef.current++;
setTimeout(connectWebSocket, reconnectInterval);
}
};
ws.onerror = (event) => {
if (onError) onError(event);
};
};
useEffect(() => {
connectWebSocket();
// Cleanup on component unmount
return () => {
if (webSocketRef.current) {
webSocketRef.current.close();
}
};
}, [url]);
const sendMessage = (message: string) => {
if (
webSocketRef.current &&
webSocketRef.current.readyState === WebSocket.OPEN
) {
webSocketRef.current.send(message);
} else {
console.error("WebSocket is not open. Unable to send message.");
}
};
return { isConnected, isReconnecting, sendMessage };
};
This hook returns two variables to track the WebSocket's state and a sendMessage
function for sending messages to the WebSocket server. By using this hook, you simplify the process of consuming data from the WebSocket server, as it handles connection management and data processing. This approach makes your code more modular and easier to maintain.
For a working Next.js example, please check the repository here
In this implementation, we’ll be creating a route handler to process initiate the request with a server that streams events back as responses.
const stream = new ReadableStream({
async start(controller) {
try {
const response = await fetch(`${URL}/api/sse`, {
headers: {
Authorization: "Bearer token",
"Cache-Control": "no-cache",
},
});
if (!response.ok) {
const errorBody = await response.text();
console.error("API error message:", errorBody);
controller.enqueue(
encodeSSE("error", `API responded with status ${response.status}`)
);
controller.close();
return;
}
const reader = response.body?.getReader();
if (!reader) {
controller.enqueue(encodeSSE("error", "No data received from API"));
controller.close();
return;
}
// Notify client of successful connection
controller.enqueue(encodeSSE("init", "Connecting..."));
while (true) {
const { done, value } = await reader.read();
if (done) break;
controller.enqueue(value);
}
controller.close();
reader.releaseLock();
} catch (error) {
console.error("Stream error:", error);
controller.enqueue(encodeSSE("error", "Stream interrupted"));
controller.close();
}
},
});
return new NextResponse(stream, {
headers: {
"Access-Control-Allow-Origin": "*",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"Content-Type": "text/event-stream",
},
status: 200,
});
Next is to create a hook to handle the streaming responses and update the UI.
const useSSE = (url: string) => {
const [isConnected, setIsConnected] = useState(false);
const [messages, setMessages] = useState<any[]>([]); // Array to store messages
const [error, setError] = useState<string | null>(null);
const eventSourceRef = useRef<EventSource | null>(null);
const reconnectAttemptsRef = useRef(0);
const maxReconnectAttempts = 5;
const connect = () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
const eventSource = new EventSource(url);
eventSourceRef.current = eventSource;
eventSource.onopen = () => {
setIsConnected(true);
setError(null);
reconnectAttemptsRef.current = 0;
};
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
setMessages((prev) => [...prev, data]); // Append new message to the array
} catch (err) {
console.error("Failed to parse message:", err);
}
};
eventSource.onerror = () => {
setIsConnected(false);
setError("Connection lost, attempting to reconnect...");
eventSource.close();
handleReconnect();
};
};
const handleReconnect = () => {
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
const retryTimeout = 1000 * Math.pow(2, reconnectAttemptsRef.current); // Exponential backoff
setTimeout(() => {
reconnectAttemptsRef.current += 1;
connect();
}, retryTimeout);
} else {
setError("Maximum reconnect attempts reached.");
}
};
useEffect(() => {
connect();
return () => {
eventSourceRef.current?.close(); // Clean up connection on unmount
};
}, [url]);
return { isConnected, messages, error };
};
This hook provides an easy way to manage a Server-Sent Events (SSE) connection within a React functional component. It is responsible for establishing a persistent connection, tracking connection state, handling incoming messages, handling reconnection logic and error management.
For a working Next.js example, please check the repository here.
For managing multiple WebSocket connections (a "connection pool"), you can create a pool manager to open, reuse, and close connections as needed.
class WebSocketPool {
private pool: Map<string, WebSocket> = new Map();
connect(url: string): WebSocket {
if (this.pool.has(url)) {
return this.pool.get(url)!;
}
const ws = new WebSocket(url);
this.pool.set(url, ws);
ws.onclose = () => {
console.log(`Connection to ${url} closed.`);
this.pool.delete(url);
};
return ws;
}
sendMessage(url: string, message: string) {
const ws = this.pool.get(url);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(message);
} else {
console.error(`WebSocket to ${url} is not open.`);
}
}
closeConnection(url: string) {
const ws = this.pool.get(url);
if (ws) {
ws.close();
this.pool.delete(url);
}
}
closeAll() {
this.pool.forEach((ws) => ws.close());
this.pool.clear();
}
}
export const webSocketPool = new WebSocketPool();
import { webSocketPool } from '../utils/webSocketPool';
const ws1 = webSocketPool.connect('ws://localhost:3000/ws1');
const ws2 = webSocketPool.connect('ws://localhost:3000/ws2');
webSocketPool.sendMessage('ws://localhost:3000/ws1', 'Hello WS1');
webSocketPool.sendMessage('ws://localhost:3000/ws2', 'Hello WS2');
// Close individual connection
webSocketPool.closeConnection('ws://localhost:3000/ws1');
// Close all connections
webSocketPool.closeAll();
The WebSocketPool
class manages WebSocket connections by storing them in a Map
, using their URLs as keys. When a connection is requested, it reuses an existing WebSocket if available or creates a new one and adds it to the pool. Messages can be sent through open connections using sendMessage
. The closeConnection
method removes and closes a specific WebSocket, while closeAll
shuts down and clears all connections, ensuring efficient management and reuse of resources.
To prevent leaks, we can create a custom hook that monitors memory usage and triggers cleanup actions when memory usage exceeds a specified threshold. Here's how you can adapt it:
import { useEffect } from "react";
const useMemoryManager = (onHighMemory: () => void, interval = 5000, threshold = 0.8) => {
useEffect(() => {
const monitorMemory = () => {
const memoryUsage = process.memoryUsage();
const heapUsedRatio = memoryUsage.heapUsed / memoryUsage.heapTotal;
if (heapUsedRatio > threshold) {
onHighMemory();
}
};
const intervalId = setInterval(monitorMemory, interval);
return () => {
clearInterval(intervalId); // Cleanup interval on unmount
};
}, [onHighMemory, interval, threshold]);
};
export default useMemoryManager;
The useMemoryManager
hook provides a way to monitor heap memory usage within the app and trigger cleanup actions when memory usage exceeds a specified threshold. It accepts three parameters: a callback function (onHighMemory
) that is executed when high memory usage is detected, an interval duration (defaulting to 5000 milliseconds) for periodic checks, and a memory threshold (defaulting to 80% of heap memory).
The hook utilizes setInterval
to repeatedly assess memory usage via process.memoryUsage()
and compares the ratio of heapUsed
to heapTotal
against the threshold. If the threshold is exceeded, the onHighMemory
callback is invoked, allowing developers to implement cleanup strategies such as closing idle connections, clearing caches, or triggering garbage collection.
Additionally, the hook ensures proper resource management by clearing the interval when the component unmounts. This makes it a practical solution for maintaining efficient memory usage and avoiding potential memory leaks in server-side or Electron-based React applications.
WebSockets |
Server Sent Events |
---|---|
Use when you need Bidirectional communication |
Use when you only need server-to-client updates |
Use when you need Real-time updates are critical |
Use when you want simpler implementation |
Building Chat applications |
Building News feeds |
Building Collaborative editing tools |
Building social media streams |
Building Real-time games |
Building dashboard updates |
Building Live trading platforms |
Building status monitoring |
Both WebSockets and SSE have their place in modern web applications. WebSockets excel in scenarios requiring bidirectional communication and low latency, while SSE is perfect for simpler, unidirectional streaming needs. The choice between them should be based on your specific use case, considering factors like:
Communication pattern requirements
Scalability needs
Browser support requirements
Development complexity tolerance
Infrastructure constraints
Remember that these technologies aren't mutually exclusive – many applications can benefit from using both, each serving different purposes within the same system.