← Back to articles

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

MethodBest ForComplexityLatency
PollingSimple dashboardsLow1-30s
Server-Sent Events (SSE)One-way updatesLow<100ms
WebSocketsBidirectional chat/collabMedium<50ms
WebRTCVideo/audio/P2PHigh<10ms
Managed serviceAny real-time needLow<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:

  1. PostgreSQL: Use LISTEN/NOTIFY or logical replication
  2. Supabase: Built-in Realtime listens to Postgres changes
  3. 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-redis adapter 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?

  1. Client detects disconnection
  2. Reconnect with exponential backoff
  3. On reconnection, fetch missed events (either by timestamp or sequence number)
  4. Merge with local state

The Bottom Line

For most apps in 2026:

  1. Start with SSE for server → client updates (simplest, most apps need only this)
  2. Use Supabase Realtime if you're already on Supabase (zero additional setup)
  3. Add WebSockets when you need bidirectional communication
  4. Use Ably/Pusher when scaling beyond one server
  5. 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.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.