Next.js WebSocket Guide
Next.js WebSocket integration requires additional setup beyond the standard API routes framework. Unlike traditional Node.js servers, Next.js does not natively support WebSocket connections through its API routes system. The framework is built primarily for HTTP request-response cycles, which creates challenges when implementing persistent bidirectional connections that WebSocket protocols require.
This limitation stems from Next.js’s architecture, which optimizes for serverless and edge deployments. While this design choice enables excellent performance for traditional web applications, it means developers need to implement custom solutions for real-time features. The approaches vary significantly depending on your deployment target, whether you are using a custom Node.js server or deploying to serverless platforms like Vercel.
Why Next.js API Routes Don’t Support WebSockets
Next.js API routes are designed as serverless functions that handle individual HTTP requests and responses. Each request creates a new execution context that terminates once the response is sent. WebSocket connections, however, require a persistent server process that maintains open connections over extended periods. This fundamental mismatch means you cannot simply add WebSocket logic to a Next.js API route and expect it to work.
The serverless nature of API routes makes them stateless by design. WebSocket connections are inherently stateful, requiring the server to track connection state, manage multiple simultaneous clients, and maintain message queues. These requirements conflict with the serverless execution model where functions spin up on demand and terminate after completing their task.
Custom Server Approach with Express and ws
The most direct solution for adding WebSocket support to Next.js involves creating a custom server that bypasses the default Next.js server. This approach gives you full control over the server lifecycle and allows you to attach WebSocket handlers alongside your Next.js application.
Here is a complete implementation using Express and the ws library:
// server.js
const express = require('express');
const next = require('next');
const { WebSocketServer } = require('ws');
const http = require('http');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const PORT = process.env.PORT || 3000;
app.prepare().then(() => {
const server = express();
const httpServer = http.createServer(server);
// Create WebSocket server
const wss = new WebSocketServer({ server: httpServer, path: '/api/ws' });
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (message) => {
console.log('Received:', message.toString());
// Echo message back to client
ws.send(JSON.stringify({
type: 'echo',
data: message.toString(),
timestamp: Date.now()
}));
// Broadcast to all clients
wss.clients.forEach((client) => {
if (client.readyState === 1) {
client.send(JSON.stringify({
type: 'broadcast',
data: message.toString()
}));
}
});
});
ws.on('close', () => {
console.log('Client disconnected');
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
});
// Handle all other routes with Next.js
server.all('*', (req, res) => {
return handle(req, res);
});
httpServer.listen(PORT, (err) => {
if (err) throw err;
console.log(`> Ready on http://localhost:${PORT}`);
});
});
Update your package.json to use the custom server:
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
}
On the client side, you can connect to the WebSocket server from your React components:
// components/WebSocketClient.tsx
'use client';
import { useEffect, useState, useRef } from 'react';
interface Message {
type: string;
data: string;
timestamp?: number;
}
export default function WebSocketClient() {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/api/ws`);
ws.onopen = () => {
console.log('WebSocket connected');
setIsConnected(true);
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages((prev) => [...prev, message]);
};
ws.onclose = () => {
console.log('WebSocket disconnected');
setIsConnected(false);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
wsRef.current = ws;
return () => {
ws.close();
};
}, []);
const sendMessage = () => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(inputValue);
setInputValue('');
}
};
return (
<div>
<div>Status: {isConnected ? 'Connected' : 'Disconnected'}</div>
<div>
{messages.map((msg, index) => (
<div key={index}>
{msg.type}: {msg.data}
</div>
))}
</div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
/>
<button onClick={sendMessage}>Send</button>
</div>
);
}
This custom server approach works well for deployments where you control the server infrastructure, such as VPS hosting, containerized deployments, or traditional cloud platforms. However, it eliminates the ability to use serverless deployments, which is one of Next.js’s key advantages.
Socket.IO Integration with Next.js
Socket.IO provides a more feature-rich alternative to raw WebSockets, with automatic reconnection, room support, and fallback mechanisms. Integrating Socket.IO with Next.js follows a similar custom server pattern:
// server.js
const express = require('express');
const next = require('next');
const { Server } = require('socket.io');
const http = require('http');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const PORT = process.env.PORT || 3000;
app.prepare().then(() => {
const server = express();
const httpServer = http.createServer(server);
const io = new Server(httpServer, {
cors: {
origin: dev ? 'http://localhost:3000' : process.env.PRODUCTION_URL,
methods: ['GET', 'POST']
}
});
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
socket.on('message', (data) => {
console.log('Received message:', data);
// Emit to sender
socket.emit('message', {
id: socket.id,
data,
timestamp: Date.now()
});
// Broadcast to all other clients
socket.broadcast.emit('message', {
id: socket.id,
data,
timestamp: Date.now()
});
});
socket.on('join-room', (room) => {
socket.join(room);
console.log(`Socket ${socket.id} joined room ${room}`);
io.to(room).emit('user-joined', {
id: socket.id,
room
});
});
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
server.all('*', (req, res) => {
return handle(req, res);
});
httpServer.listen(PORT, (err) => {
if (err) throw err;
console.log(`> Ready on http://localhost:${PORT}`);
});
});
Client-side Socket.IO implementation in TypeScript:
// hooks/useSocket.ts
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
export function useSocket(url: string) {
const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const socketInstance = io(url, {
transports: ['websocket', 'polling']
});
socketInstance.on('connect', () => {
setIsConnected(true);
console.log('Socket.IO connected');
});
socketInstance.on('disconnect', () => {
setIsConnected(false);
console.log('Socket.IO disconnected');
});
setSocket(socketInstance);
return () => {
socketInstance.close();
};
}, [url]);
return { socket, isConnected };
}
// components/ChatComponent.tsx
'use client';
import { useEffect, useState } from 'react';
import { useSocket } from '@/hooks/useSocket';
interface ChatMessage {
id: string;
data: string;
timestamp: number;
}
export default function ChatComponent() {
const { socket, isConnected } = useSocket('http://localhost:3000');
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState('');
useEffect(() => {
if (!socket) return;
socket.on('message', (message: ChatMessage) => {
setMessages((prev) => [...prev, message]);
});
return () => {
socket.off('message');
};
}, [socket]);
const sendMessage = () => {
if (socket && input.trim()) {
socket.emit('message', input);
setInput('');
}
};
return (
<div>
<div>Status: {isConnected ? 'Connected' : 'Disconnected'}</div>
<div>
{messages.map((msg, idx) => (
<div key={idx}>
<strong>{msg.id}:</strong> {msg.data}
</div>
))}
</div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
/>
<button onClick={sendMessage}>Send</button>
</div>
);
}
Socket.IO provides additional features like automatic reconnection handling, binary data support, and multiplexing through namespaces. These features make it a popular choice for production applications despite the slightly larger bundle size compared to raw WebSockets.
App Router Considerations
Next.js 13+ introduced the App Router, which brings additional considerations for WebSocket implementations. The App Router uses React Server Components by default, which run only on the server and cannot access browser APIs like WebSocket.
You must mark any component that uses WebSocket as a Client Component using the 'use client' directive:
// app/chat/page.tsx
'use client';
import { useEffect, useState } from 'react';
export default function ChatPage() {
const [ws, setWs] = useState<WebSocket | null>(null);
useEffect(() => {
const websocket = new WebSocket('ws://localhost:3000/api/ws');
setWs(websocket);
return () => {
websocket.close();
};
}, []);
// Rest of component
}
For better organization, extract WebSocket logic into custom hooks that can be reused across components:
// hooks/useWebSocket.ts
'use client';
import { useEffect, useState, useCallback } from 'react';
interface UseWebSocketOptions {
url: string;
onMessage?: (event: MessageEvent) => void;
onOpen?: () => void;
onClose?: () => void;
onError?: (error: Event) => void;
}
export function useWebSocket({
url,
onMessage,
onOpen,
onClose,
onError
}: UseWebSocketOptions) {
const [socket, setSocket] = useState<WebSocket | null>(null);
const [readyState, setReadyState] = useState<number>(WebSocket.CONNECTING);
useEffect(() => {
const ws = new WebSocket(url);
ws.onopen = () => {
setReadyState(WebSocket.OPEN);
onOpen?.();
};
ws.onmessage = (event) => {
onMessage?.(event);
};
ws.onclose = () => {
setReadyState(WebSocket.CLOSED);
onClose?.();
};
ws.onerror = (error) => {
onError?.(error);
};
setSocket(ws);
return () => {
ws.close();
};
}, [url, onMessage, onOpen, onClose, onError]);
const send = useCallback((data: string | ArrayBuffer | Blob) => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(data);
}
}, [socket]);
return { socket, readyState, send };
}
Serverless Limitations and Vercel Deployment
The most significant challenge with nextjs websocket implementations is serverless platform compatibility. Vercel, the company behind Next.js, does not support WebSocket connections in their serverless infrastructure. This limitation extends to other serverless platforms like AWS Lambda, Netlify Functions, and Cloudflare Workers in their standard configurations.
Serverless functions have execution time limits and are designed to terminate after completing a request. WebSocket connections need to remain open indefinitely, which conflicts with this model. When you deploy a Next.js application with a custom server to Vercel, the WebSocket functionality will not work because Vercel’s infrastructure does not maintain persistent connections.
For vercel websocket scenarios, you have several alternatives:
- Use a separate WebSocket server hosted on a traditional cloud platform
- Implement WebSocket functionality through a managed service
- Use HTTP-based alternatives like Server-Sent Events for one-way communication
- Deploy the entire application to a platform that supports persistent connections
If you need WebSocket functionality with a Vercel deployment, the recommended approach is to split your architecture. Deploy your Next.js application to Vercel for the HTTP layer and use a dedicated WebSocket service or server for real-time features.
Managed WebSocket Alternatives
For production applications, especially those deployed to serverless platforms, managed WebSocket services provide a practical solution. These services handle connection management, scaling, and reliability while providing simple client libraries.
Ably
Ably offers a comprehensive real-time messaging platform with WebSocket support:
// lib/ably.ts
import Ably from 'ably';
export const ably = new Ably.Realtime({
key: process.env.NEXT_PUBLIC_ABLY_KEY
});
// components/AblyChat.tsx
'use client';
import { useEffect, useState } from 'react';
import { ably } from '@/lib/ably';
export default function AblyChat() {
const [messages, setMessages] = useState<string[]>([]);
const [input, setInput] = useState('');
useEffect(() => {
const channel = ably.channels.get('chat-room');
channel.subscribe('message', (message) => {
setMessages((prev) => [...prev, message.data]);
});
return () => {
channel.unsubscribe();
};
}, []);
const sendMessage = () => {
const channel = ably.channels.get('chat-room');
channel.publish('message', input);
setInput('');
};
return (
<div>
{messages.map((msg, idx) => (
<div key={idx}>{msg}</div>
))}
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={sendMessage}>Send</button>
</div>
);
}
Pusher
Pusher provides WebSocket functionality through channels:
// lib/pusher.ts
import Pusher from 'pusher-js';
export const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, {
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!
});
// components/PusherChat.tsx
'use client';
import { useEffect, useState } from 'react';
import { pusher } from '@/lib/pusher';
export default function PusherChat() {
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
const channel = pusher.subscribe('chat');
channel.bind('message', (data: { text: string }) => {
setMessages((prev) => [...prev, data.text]);
});
return () => {
pusher.unsubscribe('chat');
};
}, []);
return (
<div>
{messages.map((msg, idx) => (
<div key={idx}>{msg}</div>
))}
</div>
);
}
Soketi
Soketi is an open-source Pusher alternative that you can self-host:
npm install @soketi/soketi-js
import Pusher from 'pusher-js';
const pusher = new Pusher('app-key', {
wsHost: 'localhost',
wsPort: 6001,
forceTLS: false,
disableStats: true,
enabledTransports: ['ws', 'wss']
});
Liveblocks
Liveblocks specializes in collaborative features:
// liveblocks.config.ts
import { createClient } from '@liveblocks/client';
export const client = createClient({
publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_KEY!
});
These managed services handle the complexity of WebSocket connection management, provide global edge networks for low latency, and integrate seamlessly with serverless Next.js deployments.
WebSocket Support in Vue, Nuxt, and SvelteKit
Similar challenges exist in other modern frameworks. For nuxt websocket implementations, Nuxt 3 faces the same serverless limitations as Next.js. You need to use Nitro’s custom server capabilities or rely on managed services.
For vue websocket and vue js websocket scenarios in standalone Vue applications, you have more flexibility since Vue itself is just a client-side library. You can connect to any WebSocket server from Vue components without framework restrictions:
// Vue 3 Composition API
import { ref, onMounted, onUnmounted } from 'vue';
export function useWebSocket(url) {
const messages = ref([]);
let socket;
onMounted(() => {
socket = new WebSocket(url);
socket.onmessage = (event) => {
messages.value.push(event.data);
};
});
onUnmounted(() => {
socket?.close();
});
return { messages };
}
For sveltekit websocket implementations, SvelteKit offers more native support through its server hooks and adapters. The Node adapter allows WebSocket integration similar to Next.js custom servers:
// hooks.server.js
import { WebSocketServer } from 'ws';
export const handleUpgrade = ({ request, upgrade }) => {
if (request.url.endsWith('/ws')) {
upgrade(request);
}
};
SvelteKit’s architecture provides clearer separation between server and client code, making WebSocket integration more straightforward than in Next.js.
Best Practices for Next.js WebSocket Implementation
When implementing WebSocket functionality in Next.js applications, follow these practices:
Use environment variables for WebSocket URLs to support different environments:
const WS_URL = process.env.NEXT_PUBLIC_WS_URL ||
(typeof window !== 'undefined'
? `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/api/ws`
: '');
Implement automatic reconnection logic to handle network interruptions:
function connectWebSocket(url: string, maxRetries = 5) {
let retries = 0;
function connect() {
const ws = new WebSocket(url);
ws.onclose = () => {
if (retries < maxRetries) {
retries++;
setTimeout(connect, 1000 * retries);
}
};
return ws;
}
return connect();
}
Handle cleanup properly to prevent memory leaks:
useEffect(() => {
const ws = new WebSocket(url);
return () => {
ws.close();
};
}, [url]);
Consider message queuing when the connection is unavailable:
const messageQueue = useRef<string[]>([]);
const sendMessage = (message: string) => {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(message);
} else {
messageQueue.current.push(message);
}
};
useEffect(() => {
if (ws?.readyState === WebSocket.OPEN && messageQueue.current.length > 0) {
messageQueue.current.forEach(msg => ws.send(msg));
messageQueue.current = [];
}
}, [ws?.readyState]);
FAQ
Can I use WebSockets in Next.js API routes?
No, Next.js API routes do not support WebSocket connections. API routes are designed as serverless functions that handle individual HTTP requests and terminate after sending a response. WebSockets require persistent connections that remain open over time, which conflicts with the serverless execution model. You must use a custom server with Express or another Node.js HTTP server to support WebSocket connections in Next.js.
Do WebSockets work when deploying Next.js to Vercel?
WebSockets do not work on Vercel’s serverless infrastructure. Vercel is optimized for serverless functions with limited execution time and does not support the persistent connections that WebSockets require. If you need real-time functionality on Vercel, use managed WebSocket services like Ably, Pusher, or Liveblocks, or deploy a separate WebSocket server on a platform that supports persistent connections like Railway, Render, or DigitalOcean.
What is the difference between Socket.IO and native WebSockets in Next.js?
Native WebSockets provide a lower-level protocol implementation with minimal overhead, while Socket.IO adds features like automatic reconnection, room support, event-based messaging, and fallback to HTTP long-polling when WebSockets are unavailable. Both require a custom Next.js server and cannot run in serverless environments. Socket.IO has a larger bundle size but provides more built-in functionality for complex real-time applications. Choose native WebSockets for simple use cases and Socket.IO when you need advanced features.
How do I implement WebSockets in Next.js App Router?
In the Next.js App Router, mark any component using WebSocket as a Client Component with the 'use client' directive at the top of the file. Extract WebSocket logic into custom hooks for reusability. Create a custom server file that combines Next.js with a WebSocket server using the ws library or Socket.IO. Update your package.json scripts to use the custom server instead of the default Next.js server. Remember that this approach eliminates serverless deployment capabilities and requires a Node.js hosting environment.