← Back to articles

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

ApproachComplexityCostScaleBest For
Managed service (Pusher, Ably)Low$$/messageUnlimitedMost apps
Supabase RealtimeLowIncludedGoodSupabase users
Socket.ioMediumSelf-hostGoodCustom real-time logic
Native WebSocketHighSelf-hostGoodMaximum control
SSE (Server-Sent Events)LowSelf-hostGoodOne-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 (ws library). 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.

Get AI tool guides in your inbox

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