How to Add Real-Time Features to Your App (2026)
Users expect live updates — chat messages, notifications, collaborative editing, live dashboards. Here's how to add real-time features to your app without overcomplicating your architecture.
The Options
| Approach | Best For | Complexity |
|---|---|---|
| Server-Sent Events (SSE) | One-way updates (notifications, feeds) | Low |
| WebSockets | Two-way communication (chat, gaming) | Medium |
| Liveblocks | Collaborative features (cursors, editing) | Low |
| Pusher/Ably | Managed real-time infrastructure | Low |
| Convex | Real-time database (auto-updates) | Low |
| PartyKit | Custom real-time logic on the edge | Medium |
Option 1: Server-Sent Events (Simplest)
For one-way updates (server → client). No library needed.
Server (Next.js API Route)
// app/api/notifications/route.ts
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
start(controller) {
const interval = setInterval(() => {
const data = JSON.stringify({ type: 'notification', message: 'New update!' })
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
}, 5000)
// Cleanup on close
return () => clearInterval(interval)
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
}
Client
useEffect(() => {
const source = new EventSource('/api/notifications')
source.onmessage = (event) => {
const data = JSON.parse(event.data)
setNotifications(prev => [data, ...prev])
}
return () => source.close()
}, [])
Best for: Live notifications, activity feeds, dashboard updates.
Option 2: WebSockets (Two-Way)
For bidirectional communication. Use Socket.io or native WebSockets.
npm install socket.io socket.io-client
Server
import { Server } from 'socket.io'
const io = new Server(3001, { cors: { origin: '*' } })
io.on('connection', (socket) => {
console.log('User connected:', socket.id)
socket.on('chat:message', (message) => {
io.emit('chat:message', { ...message, timestamp: Date.now() })
})
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id)
})
})
Client
import { io } from 'socket.io-client'
const socket = io('http://localhost:3001')
socket.on('chat:message', (message) => {
setMessages(prev => [...prev, message])
})
const sendMessage = (text: string) => {
socket.emit('chat:message', { text, userId })
}
Best for: Chat, gaming, live collaboration.
Limitation: WebSockets don't work on serverless (Vercel Functions). Need a persistent server (Fly.io, Railway) or use a managed service.
Option 3: Liveblocks (Collaboration)
For multiplayer features — cursors, presence, collaborative editing.
npm install @liveblocks/client @liveblocks/react
// liveblocks.config.ts
import { createClient } from '@liveblocks/client'
import { createRoomContext } from '@liveblocks/react'
const client = createClient({ publicApiKey: 'pk_...' })
export const { RoomProvider, useOthers, useMyPresence, useStorage } =
createRoomContext(client)
Live Cursors
function Cursors() {
const others = useOthers()
return others.map(({ connectionId, presence }) => (
<Cursor
key={connectionId}
x={presence.cursor?.x}
y={presence.cursor?.y}
color={presence.color}
/>
))
}
Collaborative State
function TodoList() {
const todos = useStorage((root) => root.todos)
const addTodo = useMutation(({ storage }, text) => {
storage.get('todos').push({ text, done: false })
}, [])
// todos auto-syncs across all users in real-time
}
Best for: Figma-like collaboration, shared whiteboards, multiplayer editing.
Option 4: Pusher (Managed WebSockets)
Works on serverless. No persistent server needed.
npm install pusher pusher-js
Server
import Pusher from 'pusher'
const pusher = new Pusher({
appId: '...', key: '...', secret: '...', cluster: 'us2',
})
// Trigger an event
await pusher.trigger('chat-room', 'new-message', {
text: 'Hello!',
userId: '123',
})
Client
import Pusher from 'pusher-js'
const pusher = new Pusher('key', { cluster: 'us2' })
const channel = pusher.subscribe('chat-room')
channel.bind('new-message', (data) => {
setMessages(prev => [...prev, data])
})
Pricing: Free: 200K messages/day. Paid: from $49/month.
Best for: Adding real-time to serverless apps without managing infrastructure.
Option 5: Convex (Real-Time Database)
The database itself is real-time. Queries automatically update when data changes.
// convex/messages.ts
export const list = query(async ({ db }) => {
return await db.query('messages').order('desc').take(50)
})
// React component — auto-updates when messages change
function Chat() {
const messages = useQuery(api.messages.list)
// messages automatically updates in real-time!
}
Best for: Apps where real-time is core (chat, dashboards, collaboration).
Decision Framework
Need: Live notifications only?
→ Server-Sent Events (zero dependencies)
Need: Chat or bidirectional communication?
→ Pusher (serverless) or WebSockets (traditional server)
Need: Collaborative editing (cursors, shared state)?
→ Liveblocks
Need: Real-time database (everything updates live)?
→ Convex
Need: Custom real-time logic at the edge?
→ PartyKit
FAQ
Can I use WebSockets on Vercel?
Not in serverless functions (they terminate). Use Pusher, Ably, or Liveblocks for managed real-time on Vercel. Or deploy WebSocket servers on Fly.io/Railway.
How many concurrent connections can I handle?
SSE/WebSockets: depends on your server (typically 10K-100K per instance). Managed services (Pusher, Liveblocks): handle scaling for you.
What about Supabase Realtime?
Supabase has built-in real-time via Postgres changes. Good if you're already using Supabase. Limited compared to dedicated real-time tools.
Do I need real-time?
Maybe not. Polling every 5-10 seconds works for many use cases (dashboards, feeds) and is much simpler. Add real-time only when users expect instant updates.
Bottom Line
Start with the simplest option. SSE for one-way updates. Pusher for serverless two-way. Liveblocks for collaboration. Convex for real-time-first apps. Don't over-engineer — polling is fine for most use cases. Add real-time when users notice the delay.