How to Add Real-Time Features to Your App (2026 Guide)
Users expect real-time: live notifications, collaborative editing, instant messaging, real-time dashboards. Here's how to add real-time features to any web app in 2026.
Choose Your Approach
| Method | Best For | Complexity | Latency |
|---|---|---|---|
| Polling | Simple dashboards | Low | 1-30s |
| Server-Sent Events (SSE) | One-way updates | Low | <100ms |
| WebSockets | Bidirectional chat/collab | Medium | <50ms |
| WebRTC | Video/audio/P2P | High | <10ms |
| Managed service | Any real-time need | Low | <100ms |
Option 1: Polling (Simplest)
Periodically fetch new data. Simple but inefficient.
// React example
function usePoll(url: string, intervalMs = 5000) {
const [data, setData] = useState(null);
useEffect(() => {
const fetch = async () => {
const res = await fetch(url);
setData(await res.json());
};
fetch();
const id = setInterval(fetch, intervalMs);
return () => clearInterval(id);
}, [url, intervalMs]);
return data;
}
When to use: Dashboards that update every 5-30 seconds. Simple status checks. Low-traffic apps where the overhead doesn't matter.
When NOT to use: Chat, collaboration, or anything needing sub-second updates.
Option 2: Server-Sent Events (SSE)
Server pushes updates to the client over HTTP. Simple, reliable, and often overlooked.
// Server (Node.js/Express)
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const sendEvent = (data: any) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
// Send updates
const interval = setInterval(() => {
sendEvent({ type: 'heartbeat', time: Date.now() });
}, 30000);
// Listen for data changes
const handler = (event: any) => sendEvent(event);
eventEmitter.on('update', handler);
req.on('close', () => {
clearInterval(interval);
eventEmitter.off('update', handler);
});
});
// Client
const events = new EventSource('/events');
events.onmessage = (event) => {
const data = JSON.parse(event.data);
// Handle update
};
events.onerror = () => {
// Auto-reconnects by default!
};
Advantages over WebSockets:
- Works over standard HTTP (no upgrade needed)
- Auto-reconnection built into the browser API
- Works through most proxies and CDNs
- Simpler server implementation
- Sufficient for most "push" use cases
When to use: Notifications, live feeds, dashboards, stock tickers, progress updates — any one-way server-to-client push.
When NOT to use: Chat or collaboration where the client needs to send frequent messages back.
Option 3: WebSockets
Full bidirectional communication. The standard for real-time apps.
// Server (using ws library)
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
const data = JSON.parse(message.toString());
// Broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
});
});
});
// Client
const ws = new WebSocket('wss://your-server.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'join', room: 'general' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Handle incoming message
};
// Add reconnection logic
ws.onclose = () => {
setTimeout(() => reconnect(), 1000);
};
When to use: Chat applications, collaborative editing, multiplayer games, real-time bidirectional communication.
Considerations:
- Need reconnection logic (WebSocket API doesn't auto-reconnect)
- Scaling requires sticky sessions or a pub/sub layer (Redis)
- Some proxies/firewalls may interfere
Option 4: Managed Real-Time Services
Skip the infrastructure and use a service.
Supabase Realtime
// Listen to database changes
const channel = supabase
.channel('todos')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'todos' },
(payload) => {
console.log('New todo:', payload.new);
}
)
.subscribe();
Best for: Apps already using Supabase. Database change notifications.
Ably
Full-featured real-time messaging platform with pub/sub, presence, and history. Best for: Production apps needing guaranteed delivery, presence, and message history.
Pusher
Simple pub/sub messaging with channels and events. Best for: Adding notifications and simple real-time features to existing apps.
Convex
Real-time database where queries automatically update when data changes. Best for: Apps where all data should be real-time by default.
PartyKit / Cloudflare Durable Objects
Stateful edge computing for multiplayer and collaborative features. Best for: Collaborative apps, multiplayer experiences, shared state.
Architecture Patterns
Pattern 1: Database → SSE/WebSocket
Most common. Database changes trigger real-time updates.
Client action → API → Database → Change notification →
Broadcast to connected clients via SSE/WebSocket
Implementation:
- PostgreSQL: Use
LISTEN/NOTIFYor logical replication - Supabase: Built-in Realtime listens to Postgres changes
- Convex: Automatic — queries re-run when data changes
Pattern 2: Pub/Sub
Decouple publishers from subscribers using Redis, Ably, or similar.
Client A sends message → Server publishes to Redis channel →
All servers subscribed to channel → Broadcast to their connected clients
Best for: Multi-server deployments where you need to scale horizontally.
Pattern 3: CRDT-Based (Conflict-Free)
For collaborative editing where multiple users modify the same document.
Client A edits → Generate CRDT operation → Broadcast to peers →
Each client merges operations → Consistent state guaranteed
Tools: Yjs, Automerge, Liveblocks, PartyKit
Scaling Real-Time
Single Server (<1K Connections)
Direct WebSocket/SSE connections. No additional infrastructure needed.
Multi-Server (1K-100K Connections)
Add a pub/sub layer:
- Redis Pub/Sub: Simple, fast. Add
socket.io-redisadapter or manual pub/sub. - Ably/Pusher: Managed pub/sub. No Redis to manage.
Global Scale (100K+ Connections)
- Cloudflare Durable Objects: Stateful edge compute, global distribution
- Ably: Enterprise-grade global messaging
- Custom: Redis Cluster + regional WebSocket servers
FAQ
SSE or WebSocket?
SSE for server-to-client push (notifications, feeds, dashboards). WebSocket for bidirectional communication (chat, collaboration). SSE is simpler and sufficient for 80% of real-time needs.
How many WebSocket connections can one server handle?
A Node.js server can handle 10K-50K concurrent WebSocket connections with proper tuning (increased file descriptors, event-driven handling). A Go or Rust server can handle 100K+.
Do I need Socket.IO or can I use raw WebSockets?
Raw WebSockets are fine for most cases. Socket.IO adds auto-reconnection, rooms, and fallback to polling — useful features but adds bundle size and complexity. In 2026, raw WebSocket + a small reconnection wrapper is usually sufficient.
How do I handle offline/reconnection?
- Client detects disconnection
- Reconnect with exponential backoff
- On reconnection, fetch missed events (either by timestamp or sequence number)
- Merge with local state
The Bottom Line
For most apps in 2026:
- Start with SSE for server → client updates (simplest, most apps need only this)
- Use Supabase Realtime if you're already on Supabase (zero additional setup)
- Add WebSockets when you need bidirectional communication
- Use Ably/Pusher when scaling beyond one server
- Use Yjs/Liveblocks for collaborative editing specifically
Don't over-engineer. Most "real-time" features only need SSE or polling. Save WebSockets for true bidirectional needs.