How to Add WebSockets to Your App (2026 Guide)
Your users expect real-time: live chat, notifications, collaborative editing, live dashboards, presence indicators. All of these need WebSockets (or something like them).
Here's how to add real-time features to your app, from the simplest approach to production-grade infrastructure.
Choose Your Approach
| Approach | Complexity | Cost | Scale | Best For |
|---|---|---|---|---|
| Managed service (Pusher, Ably) | Low | $$/message | Unlimited | Most apps |
| Supabase Realtime | Low | Included | Good | Supabase users |
| Socket.io | Medium | Self-host | Good | Custom real-time logic |
| Native WebSocket | High | Self-host | Good | Maximum control |
| SSE (Server-Sent Events) | Low | Self-host | Good | One-way updates |
Option 1: Managed Services (Fastest)
Pusher
Pusher is the most popular managed WebSocket service. Add real-time in 15 minutes.
Server (Node.js):
import Pusher from 'pusher';
const pusher = new Pusher({
appId: 'YOUR_APP_ID',
key: 'YOUR_KEY',
secret: 'YOUR_SECRET',
cluster: 'us2',
});
// Trigger an event
app.post('/api/message', async (req, res) => {
await pusher.trigger('chat', 'new-message', {
user: req.body.user,
text: req.body.text,
});
res.json({ ok: true });
});
Client (React):
import Pusher from 'pusher-js';
import { useEffect, useState } from 'react';
function Chat() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const pusher = new Pusher('YOUR_KEY', { cluster: 'us2' });
const channel = pusher.subscribe('chat');
channel.bind('new-message', (data) => {
setMessages(prev => [...prev, data]);
});
return () => pusher.disconnect();
}, []);
return <MessageList messages={messages} />;
}
Pricing: Free tier (200K messages/day), paid from $49/month.
Ably
Ably is the enterprise alternative to Pusher with stronger guarantees.
Key advantages over Pusher:
- Message ordering guarantees
- Message persistence and history
- Presence (who's online) built-in
- Global edge network for lower latency
- 99.999% uptime SLA
Pricing: Free tier (6M messages/month), paid from $29/month.
When to Use Managed Services
- You want real-time working in hours, not days
- You don't want to manage WebSocket infrastructure
- Your real-time needs are standard (chat, notifications, live updates)
- You're on serverless (Vercel, Netlify) where persistent connections aren't possible
Option 2: Supabase Realtime
If you're using Supabase, real-time is built in. Subscribe to database changes directly.
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
// Subscribe to new messages in the 'messages' table
const channel = supabase
.channel('messages')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages' },
(payload) => {
console.log('New message:', payload.new);
}
)
.subscribe();
Advantages:
- Zero additional setup (if you're on Supabase)
- Database-driven: changes to the database automatically push to clients
- Presence and broadcast channels available
- Row-Level Security applies to real-time subscriptions
Limitations:
- Tied to Supabase
- PostgreSQL change events have slight latency vs. direct WebSocket messages
- High-frequency updates (>100/second per channel) may need optimization
Option 3: Socket.io (Self-Hosted)
Socket.io is the most popular WebSocket library for Node.js. It adds reliability features on top of native WebSockets.
Server:
import { createServer } from 'http';
import { Server } from 'socket.io';
const httpServer = createServer();
const io = new Server(httpServer, {
cors: { origin: 'http://localhost:3000' }
});
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('join-room', (roomId) => {
socket.join(roomId);
});
socket.on('send-message', (data) => {
io.to(data.roomId).emit('new-message', {
user: data.user,
text: data.text,
timestamp: Date.now(),
});
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
httpServer.listen(3001);
Client (React):
import { io } from 'socket.io-client';
import { useEffect, useState } from 'react';
const socket = io('http://localhost:3001');
function Chat({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
socket.emit('join-room', roomId);
socket.on('new-message', (message) => {
setMessages(prev => [...prev, message]);
});
return () => {
socket.off('new-message');
};
}, [roomId]);
const sendMessage = (text) => {
socket.emit('send-message', { roomId, user: 'me', text });
};
return <ChatUI messages={messages} onSend={sendMessage} />;
}
Socket.io advantages over native WS:
- Automatic reconnection
- Fallback to long-polling if WebSocket fails
- Room/namespace support
- Binary data support
- Acknowledgements (confirm message receipt)
Scaling Socket.io:
- Use the Redis adapter (
@socket.io/redis-adapter) for multi-server deployments - Sticky sessions or Redis for session affinity
- Consider Socket.io's admin UI for monitoring
Option 4: Native WebSocket API
For maximum control and minimum overhead, use the native WebSocket API.
Server (Node.js with ws):
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 3001 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const message = JSON.parse(data);
// Broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
});
});
Client:
const ws = new WebSocket('ws://localhost:3001');
ws.onopen = () => console.log('Connected');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Handle incoming message
};
ws.onclose = () => {
// Implement reconnection logic
setTimeout(() => connectWebSocket(), 1000);
};
When to use native WS:
- You need the smallest possible overhead
- You want to implement a custom protocol
- Socket.io's features are overkill
- You're building infrastructure, not an app feature
What you'll need to build yourself:
- Reconnection logic
- Heartbeat/ping-pong
- Room/channel management
- Authentication
- Message serialization
- Error handling and recovery
Option 5: Server-Sent Events (SSE)
If you only need server→client updates (no bidirectional communication), SSE is simpler than WebSockets.
Server:
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const sendEvent = (data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
// Send updates
const interval = setInterval(() => {
sendEvent({ type: 'heartbeat', time: Date.now() });
}, 30000);
req.on('close', () => clearInterval(interval));
});
Client:
const eventSource = new EventSource('/api/events');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
// Handle update
};
SSE advantages:
- Simpler than WebSockets
- Works over HTTP (no special server support needed)
- Automatic reconnection built into the browser API
- Works with HTTP/2 multiplexing
SSE limitations:
- One-directional only (server → client)
- Text-only (no binary data)
- Limited to ~6 connections per domain in HTTP/1.1
- No built-in room/channel concept
Use SSE for: Live notifications, activity feeds, dashboard updates, progress indicators, stock tickers.
Common Real-Time Patterns
Presence (Who's Online)
// Socket.io example
io.on('connection', (socket) => {
socket.on('go-online', (userId) => {
onlineUsers.set(userId, socket.id);
io.emit('presence-update', Array.from(onlineUsers.keys()));
});
socket.on('disconnect', () => {
// Remove from online users
for (const [userId, socketId] of onlineUsers) {
if (socketId === socket.id) onlineUsers.delete(userId);
}
io.emit('presence-update', Array.from(onlineUsers.keys()));
});
});
Typing Indicators
socket.on('typing-start', (data) => {
socket.to(data.roomId).emit('user-typing', { user: data.user });
});
socket.on('typing-stop', (data) => {
socket.to(data.roomId).emit('user-stopped-typing', { user: data.user });
});
Live Cursors (Collaboration)
socket.on('cursor-move', (data) => {
socket.to(data.documentId).emit('cursor-update', {
user: data.user,
x: data.x,
y: data.y,
});
});
FAQ
Do I need WebSockets for my app?
If your app has any of these, yes: chat, notifications, live updates, collaboration, dashboards with real-time data, or presence indicators. If your app is read-only content, no.
WebSockets vs polling?
WebSockets are more efficient for frequent updates (real-time). Polling is simpler for infrequent updates (check every 30 seconds). Never use short-interval polling (<5 seconds) — use WebSockets instead.
Can I use WebSockets on Vercel/serverless?
Not directly (serverless functions are stateless). Use a managed service (Pusher, Ably), Supabase Realtime, or a separate WebSocket server (Railway, Fly.io, Render).
How many concurrent connections can one server handle?
A single Node.js server can handle 10,000-100,000 concurrent WebSocket connections depending on hardware and message frequency. For more, use horizontal scaling with Redis for pub/sub coordination.
The Verdict
- Most apps: Use Pusher or Ably. Real-time in 15 minutes, no infrastructure to manage.
- Supabase users: Use Supabase Realtime. It's already there.
- Custom real-time logic: Use Socket.io. Best balance of features and control.
- Server→client only: Use SSE. Simpler than WebSockets for one-directional updates.
- Maximum control: Use native WebSocket (
wslibrary). Only if you know what you're building yourself.
Start with a managed service. Migrate to self-hosted Socket.io only when you need custom logic that the managed service can't handle.