“A distributed system is one in which the failure of a computer you didn’t even know existed can render your own computer unusable.” : Leslie Lamport
Real-time communication is a core requirement for modern applications - chat systems, live dashboards, notifications, and collaborative tools all depend on it.
Many developers assume they need separate backend and frontend services to implement WebSockets. That’s not true.
In this article, we’ll build a fully working WebSocket system inside a single Next.js application, using:
- A custom Node.js server
- The ws WebSocket library
- A clean, event-based messaging protocol
We’ll fully break down the server implementation and explain how a React frontend consumes it - without turning the article into a UI tutorial.
Why WebSockets (and Not API Polling)?
HTTP-based polling:
- Wastes bandwidth
- Introduces latency
- Scales poorly
WebSockets:
- Keep a persistent connection
- Support bi-directional communication
- Allow the server to push updates instantly
If your app needs live updates, WebSockets are the correct tool. Anything else is a workaround.
Why Use WebSockets Inside Next.js?
Because:
- You already have a Node runtime
- You can serve UI and real-time logic together
- You avoid managing multiple repos and deployments
Important constraint (we’ll be honest about it):
This approach requires a Node-based deployment (Docker, VM, Railway, Render).
It does not work on serverless platforms like Vercel.
Index
- Architecture Overview
- Why a Custom Server Is Required
- Setting Up the Custom Next.js Server
- Designing the WebSocket Message Protocol
- Tracking Connected Users
- Creating the HTTP Server
- Creating the WebSocket Server
- Handling HTTP → WebSocket Upgrade
- Broadcasting Messages
- Managing User Presence
- Handling Client Connections
- Error Handling and Cleanup
- How the Frontend Uses This Server
- Why This Architecture Makes Sense
- Watch Out For
- Interesting Facts
- Next Steps You Can Take
- FAQ
- Conclusion
1. Architecture Overview
Browser (React / Next.js)
↕ WebSocket (/ws)
Custom Next.js Server (Node.js)
├─ Next.js request handler
├─ WebSocket upgrade handler
└─ WebSocket message broker
One process. One app. Real-time enabled.
2. Why a Custom Server Is Required
Next.js API routes are serverless by default:
- No persistent process
- No long-lived connections
- WebSockets break immediately
To use WebSockets, we must:
- Run Next.js inside a custom Node HTTP server
- Manually handle WebSocket upgrades
This is not a hack - it’s the officially supported approach for advanced use cases.
3. Setting Up the Custom Server
import { createServer } from 'http';
import next from 'next';
import { WebSocketServer, WebSocket } from 'ws';
import { parse } from 'url';
What’s happening here:
- http → creates the base Node server
- next → boots Next.js manually
- ws → low-level WebSocket implementation
- url → used to inspect upgrade requests
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
This initializes Next.js exactly as it would run internally, but gives us control over the server lifecycle.
4. Defining the Message Protocol
interface ChatMessage {
type: 'message' | 'join' | 'leave' | 'userList';
username?: string;
content?: string;
users?: string[];
timestamp: number;
}
This is critical.
Instead of sending raw strings, we use a typed message protocol:
- Predictable
- Extensible
- Easy to validate
- Frontend-friendly
Every message has:
- A type
- A timestamp
- Optional payload fields
This is how real systems are built.
5. Tracking Connected Users
const users = new Map<WebSocket, string>();
Why a Map?
- Each WebSocket connection is unique
- We can associate metadata (username) with it
- Easy cleanup on disconnect
This structure represents in-memory presence tracking.
6. Creating the HTTP Server
const server = createServer((req, res) => {
handle(req, res);
});
All normal HTTP requests:
- Pages
- Assets
- API routes
Are still handled by Next.js.
7. Creating the WebSocket Server (Correctly)
const wss = new WebSocketServer({ noServer: true });
This is an important design decision.
Why noServer: true?
- We don’t want to hijack all connections
- Next.js itself uses WebSockets (HMR)
- We want full control over which requests upgrade
“Simplicity is hard work, but it pays off.” : Rich Hickey
8. Handling the WebSocket Upgrade Manually
server.on('upgrade', (request, socket, head) => {
const { pathname } = parse(request.url || '');
if (pathname === '/ws') {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
}
});
What this does:
- Intercepts upgrade requests
- Accepts only /ws
- Leaves everything else untouched
This prevents breaking:
- Next.js Hot Module Reloading
- Other internal WebSocket usage
- This is a production-grade pattern.
9. Broadcasting Messages
function broadcast(message: ChatMessage) {
const data = JSON.stringify(message);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
This turns the server into a real-time event hub:
- One message in
- Many clients updated instantly
10. Broadcasting User Presence
function broadcastUserList() {
const userList = Array.from(users.values());
broadcast({
type: 'userList',
users: userList,
timestamp: Date.now(),
});
}
Presence is treated as state, not messages.
This keeps the frontend simple and consistent.
11. Handling Client Connections
wss.on('connection', (socket) => {
socket.on('message', ...)
socket.on('close', ...)
});
Each connection:
- Can join
- Can send message
- Will trigger leave on disconnect
This is the core event loop of the system.
Join Event
- Assign username
- Store in Map
- Notify everyone
- Broadcast updated user list
Message Event
- Validate sender
- Broadcast to all clients
Leave Event
- Remove from Map
- Notify users
- Re-sync presence list
This is clean, explicit, and debuggable.
12. Error Handling and Cleanup
socket.on('error', ...)
socket.on('close', ...)
Why this matters:
- Prevents memory leaks
- Ensures stale connections don’t linger
- Keeps presence data accurate
Real-time systems fail silently if you ignore this.
13. How the Frontend Uses This Server (Conceptual)
The frontend does not need to know server internals.
It only needs to:
- Open a WebSocket connection to /ws
- Send a join message once connected
- Listen for incoming messages
- Update UI based on type
Typical Flow
- User opens page
- Browser connects to
ws(s)://your-domain/ws
Frontend sends:
{ "type": "join", "username": "Ankit" }
- Server broadcasts join + user list
- Messages flow both ways in real time
The UI is just a consumer of events.
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” : Martin Fowler
14. Why This Architecture Makes Sense
- One deployment
- One codebase
- Explicit protocol
- Clear separation of concerns
- Easy to extend with auth, rooms, or persistence
This is how real chat systems start.
15. Watch Out For
- Serverless platforms
- No reconnection logic on client
- No message validation
- No scaling strategy (Redis needed for multi-instance)
WebSockets are powerful - but unforgiving.
16. Interesting Facts
- WebSockets reduce network overhead by up to 90% compared to short-interval HTTP polling because headers are exchanged only once per connection.https://websocket.org/comparisons/long-polling
- Large-scale applications like Slack, Discord, and Figma rely heavily on WebSockets to power real-time messaging, presence, and collaboration features.https://ably.com/topic/what-are-websockets-used-for
- A single well-tuned Node.js server can handle tens of thousands of concurrent WebSocket connections due to its event-driven, non-blocking architecture.https://ably.com/topic/the-challenge-of-scaling-websockets
- WebSockets are built on top of TCP, making them more reliable for ordered message delivery compared to many custom real-time solutions.https://en.wikipedia.org/wiki/WebSocket?
- Real-time updates have been shown to improve perceived application performance and user engagement, even when backend response times remain the same.https://medium.com/towardsdev/real-time-features-websockets-vs-server-sent-events-vs-polling-e7b3d07e6442
17. Next Steps You Can Take
- Add authentication (JWT on join)
- Implement rooms or channels
- Persist messages to DB
- Add Redis Pub/Sub for scaling
- Introduce rate limiting
18. FAQ
1. Can this WebSocket setup run on Vercel?
No. Vercel’s serverless functions do not support long-lived connections. This setup requires a persistent Node.js process running on a VM or container-based platform.
2. Do I need a separate backend for WebSockets?
Not necessarily. As shown in this article, WebSockets can live inside the same Next.js application using a custom server.
3. Is this approach production-ready?
Yes. This architecture is production-safe when deployed on a Node-based platform and enhanced with proper authentication, validation, and scaling strategies.
4. How do I scale this to multiple servers?
You need a shared pub/sub layer (Redis, NATS, Kafka). Without it, each server instance will only broadcast to its own connected clients.
5. Should I use Socket.IO instead of ws?
Socket.IO adds reconnection, fallback transports, and rooms - but also extra overhead.
If you want maximum control and performance, ws is the better choice.
6. How do I secure WebSocket connections?
- Use wss:// behind HTTPS
- Authenticate users during the join event
- Validate every incoming message
- Apply rate limiting to prevent abuse
7. Can this be used for more than chat?
Absolutely. The same architecture works for:
- Live notifications
- Activity feeds
- Collaborative editing
- Real-time dashboards
- Multiplayer game state sync
19. Conclusion
You can build a clean, scalable WebSocket system inside a single Next.js application - if you respect the constraints and design it properly.
By using a custom Node server, explicit upgrade handling, and a structured message protocol, you get:
- Real-time performance
- Predictable behavior
- Production-ready architecture
If your app needs live updates, this approach is not a workaround - it’s the right tool.
About the Author: Ankit is a full-stack developer at AddWebSolution and AI enthusiast who crafts intelligent web solutions with PHP, Laravel, and modern frontend tools.
Top comments (0)