Skip to content

Backend

WebSocket vs SSE vs Supabase Realtime — Which One To Use

Need real-time data? Three tools for the job. Raw WebSocket gives full bidirectional control but you manage the server and scaling. SSE (Server-Sent Events) is simpler for one-way server-to-client streaming. Supabase Realtime wraps both primitives and ships free with your database. Aidxn rule: use Supabase for 95% of SaaS features, SSE for LLM token streaming, raw WebSocket only when you control the infra and need bidi at scale. Decision tree, three setup patterns, and 6 FAQs.

🔌 📡

You've decided your app needs real-time updates. Data changes on the server, and users see it instantly without refreshing. Three patterns exist: raw WebSocket, Server-Sent Events (SSE), and Supabase Realtime. If you've lived under a rock, here's the 30-second recap. WebSocket opens a persistent bidirectional tunnel between client and server — perfect for chat, live games, or anything that needs server→client and client→server messages. SSE is simpler: client opens a one-way stream from server, and the server pushes data down. Perfect for notifications, live pricing updates, or AI token streaming. Supabase Realtime is a managed layer that wraps WebSocket and gives you broadcast, presence, and database change subscriptions with zero infra. The internet collectively lost its mind over "serverless everything", but the real win is: you pick the abstraction level that matches your problem. This post covers all three, decision tree, and real patterns from production Velocity dashboards.

The Three Approaches Compared

Real-time data boils down to two questions: who initiates messages, and who manages the infrastructure?

Raw WebSocket: Bidirectional, Full Control

WebSocket is a browser standard that upgrades an HTTP connection to a persistent, bidirectional socket. The server keeps it open, and messages can flow both ways at any time with zero latency. You own the connection, you own the server, you own scaling. Use WebSocket when you need: true bidirectionality (client sends, server sends, both without asking), microsecond-level latency (think high-frequency trading), or custom binary protocols (game state, sensor data). The catch: you're running a WebSocket server (Node.js, Rust, Go, take your pick), you're handling reconnections, you're scaling horizontally, you're debugging half-closed sockets at 2am. WebSocket is powerful but comes with infrastructure tax.

Server-Sent Events: One-Way, Browser-Native, Simple

SSE is the forgotten API. It lives in the browser as EventSource and lets the server push messages down an open HTTP connection. Unlike WebSocket, it's one-way: server→client only. The client can't send messages back (well, it can send separate HTTP requests, but that's not SSE). SSE is perfect for: live streams (like Claude's token streaming), push notifications, live dashboard updates from a data source you control. The magic: it's HTTP under the hood, so it works with any web server (Express, Python, ASP.NET) and respects CORS natively. Reconnection is automatic — the browser's EventSource API retries if the connection drops. For 80% of real-time features, SSE is simpler than WebSocket because you don't need bidirectional traffic.

Supabase Realtime: Managed WebSocket with Database Awareness

Supabase Realtime is a hosted layer that abstracts away WebSocket management. It ships with every Supabase database and gives you three primitives: broadcast (fire-and-forget messages), presence (track online users), and postgres_changes (stream database row updates). You open a single persistent connection to Supabase's Realtime server, and it handles reconnection, scaling, and message routing for you. Use Supabase Realtime when: you need bidirectional messaging but don't want to manage a server, you're already using Supabase for auth/database, you want RLS-enforced subscriptions (users see only their data), or you need tight database integration. Pricing is per message, not per connection, so it scales with usage, not concurrent users. For most SaaS, Supabase Realtime is the goldilocks option: managed infrastructure + database awareness + zero setup.

Decision Tree: Which One For Your Feature

Use Supabase Realtime if: You need bidirectional real-time data and you're already on Supabase. Example: dashboards, collaborative editing, live notifications tied to database rows. This is the default for Velocity dashboards and Aidxn projects. It's managed (no server to run), RLS-scoped (automatic permission checking), and free for small projects.

Use SSE if: You need one-way server-to-client streaming and want maximum simplicity. Example: AI token streaming (like an LLM chat endpoint), live price tickers from your backend, or system notifications. SSE is HTTP-native, works with any backend, and is trivial to implement. Clients reconnect automatically.

Use raw WebSocket if: You control both client and server infrastructure, you need bidirectional messaging at scale, and you're willing to manage server code. Example: multiplayer games, real-time whiteboard/collaborative drawing, or a custom trading terminal. Only pick this if Supabase Realtime's latency (50–200ms) or message pricing doesn't fit, or you need binary protocols.

Supabase Realtime Pattern: Broadcast + Presence + postgres_changes

Here's how to wire Supabase Realtime in a React component (the Velocity dashboard pattern):

import { useEffect, useState } from 'react';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

export function LiveDashboard({ orgId, userId }) {
  const [onlineUsers, setOnlineUsers] = useState([]);
  const [kpis, setKpis] = useState([]);

  useEffect(() => {
    const channel = supabase
      .channel(`org-${orgId}`, {
        config: { presence: { key: `user-${userId}` } }
      })
      // Track presence: who's viewing now
      .on('presence', { event: 'sync' }, () => {
        const state = channel.presenceState();
        setOnlineUsers(Object.values(state).flat());
      })
      // Stream database changes: live KPIs
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'daily_metrics',
          filter: `organization_id=eq.${orgId}`
        },
        (payload) => {
          setKpis((prev) => [...prev, payload.new]);
        }
      )
      // Broadcast events: UI state
      .on('broadcast', { event: 'export_done' }, (payload) => {
        console.log(`${payload.user_name} exported the report`);
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          // Announce you're here
          await channel.track({
            user_id: userId,
            email: userEmail,
            color: '#3b82f6'
          });
        }
      });

    return () => {
      channel.unsubscribe();
    };
  }, [orgId, userId]);

  return (
    

Online now: {onlineUsers.map(u => u.email).join(', ')}

{kpis.map((kpi) => (
{kpi.revenue} → {kpi.metric}
))}
); }

One subscription, three event types. When a KPI is inserted (postgres_changes), your chart updates. When a colleague joins (presence), their name appears. When data is exported (broadcast), everyone sees it. Supabase routes everything over one WebSocket, so latency is low and scaling is Supabase's problem.

SSE Pattern: Streaming AI Responses

Here's how to stream tokens from an LLM API using SSE (the common pattern for Claude token streaming):

// Server (Node.js + Express)
app.get('/api/stream-response', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const response = await fetch('https://api.anthropic.com/messages', {
    method: 'POST',
    body: JSON.stringify({
      model: 'claude-opus-4-1',
      messages: [{ role: 'user', content: 'Explain real-time patterns...' }],
      stream: true
    })
  });

  // Pipe the streaming response back to client
  for await (const chunk of response.body) {
    const line = chunk.toString();
    res.write(`data: ${line}\n\n`);
  }

  res.end();
});

// Client (React)
export function AiStreamingChat() {
  const [response, setResponse] = useState('');

  const handleStream = () => {
    const eventSource = new EventSource('/api/stream-response');

    eventSource.onmessage = (event) => {
      const token = JSON.parse(event.data)?.content;
      setResponse((prev) => prev + token);
    };

    eventSource.onerror = () => {
      eventSource.close();
    };
  };

  return (
    

{response}

); }

SSE is trivial: set the Content-Type header to text/event-stream, write lines as data: {message}\n\n, and the browser's EventSource API handles reconnection. No server state, no WebSocket library, just HTTP streams.

Raw WebSocket Pattern: Multiplayer Game State

Raw WebSocket is useful when you need bidirectional, bidi at scale, and custom binary protocols. Here's a simplified example:

// Server (Node.js + ws library)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const clients = new Map();

wss.on('connection', (ws) => {
  const clientId = Math.random().toString(36);
  clients.set(clientId, ws);

  ws.on('message', (message) => {
    const { type, position } = JSON.parse(message);

    // Broadcast position update to all other clients
    clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({
          clientId,
          type: 'player_moved',
          position
        }));
      }
    });
  });

  ws.on('close', () => {
    clients.delete(clientId);
    // Notify others that player left
    clients.forEach((client) => {
      client.send(JSON.stringify({
        clientId,
        type: 'player_left'
      }));
    });
  });
});

// Client (React)
export function MultiplayerGame() {
  const [otherPlayers, setOtherPlayers] = useState({});

  useEffect(() => {
    const ws = new WebSocket('ws://localhost:8080');

    ws.onmessage = (event) => {
      const { clientId, type, position } = JSON.parse(event.data);
      if (type === 'player_moved') {
        setOtherPlayers((prev) => ({
          ...prev,
          [clientId]: position
        }));
      }
    };

    return () => ws.close();
  }, []);

  return (
    
      {/* Render game with otherPlayers positions */}
    
  );
}

Raw WebSocket gives you full control: define your message types, implement custom routing, scale with multiple servers + a message queue. The trade-off is you manage every detail.

Latency, Pricing, and Scaling Comparison

Latency: Raw WebSocket (<50ms), Supabase Realtime (50–200ms, database queries add time), SSE (50–500ms depending on backend). Raw WebSocket wins on speed but only if you're optimizing for microseconds. For dashboards and notifications, the differences are invisible.

Pricing: Raw WebSocket (server cost + bandwidth), Supabase Realtime (per-message pricing, ~$1/M messages), SSE (server cost + bandwidth, often bundled with backend). Supabase scales with usage, not concurrent connections. If you have 100 users checking a dashboard hourly, you're in the cents-per-day range.

Scaling: Raw WebSocket requires load balancers, sticky sessions, and a message queue (Redis, RabbitMQ) to fan messages across servers. Supabase Realtime scaling is handled for you. SSE is simple if your backend is stateless; complex if you need to route messages across multiple server instances.

The Aidxn Rule: 95% of SaaS Uses Supabase Realtime

For most web applications, Supabase Realtime is the right tool. It's managed (no server to run), RLS-scoped (automatic permission checking), and free for small projects. Use it for dashboards, collaborative editing, notifications, and anything tied to your database. The latency (50–200ms) is fine for human-scale interactions. Only reach for SSE or raw WebSocket when Supabase doesn't fit: SSE for AI streaming or one-way data flows, raw WebSocket for games or microsecond trading. Velocity X uses Supabase Realtime for all dashboard features — broadcast for notifications, presence for online tracking, postgres_changes for live KPIs. Zero custom server code.

Six FAQs

Can I use WebSocket in Next.js API routes?

No. Next.js API routes are HTTP-only and don't support WebSocket upgrades. You'd need a separate WebSocket server (like ws or Socket.IO running on its own port). Supabase Realtime sidesteps this — just use the client library, no server code needed.

Is SSE safe for sensitive data?

SSE travels over HTTPS like any HTTP request, so it's encrypted in transit. Implement authentication (JWT tokens) and authorization (RLS policies if using Supabase) to restrict who receives what data. The one-way nature of SSE doesn't make it less or more secure than WebSocket — it's how you authenticate that matters.

What happens if a Supabase Realtime connection drops?

The client library reconnects automatically. You miss events that happened while offline. If you need a replay, query the database to fetch recent changes (e.g., changes in the last hour) and then re-subscribe. For critical data, use postgres_changes instead of broadcast so updates persist in the database.

Can I use raw WebSocket with serverless (Lambda, Functions, etc.)?

Serverless platforms don't keep connections open. WebSocket needs a persistent server, so Lambda and Cloud Functions won't work. Supabase Realtime or managed WebSocket services (like AWS AppSync) are your options.

How do I rate-limit or throttle messages?

Supabase Realtime has built-in rate limiting (configurable per message type). For SSE, throttle on the server side before writing to the response. For raw WebSocket, implement throttling in your server code (e.g., allow max 10 messages/second per client). For high-frequency data (like stock prices), aggregate updates server-side and send every 100ms instead of every tick.

Should I use Socket.IO instead of raw WebSocket?

Socket.IO is a wrapper around WebSocket that adds fallbacks (polling) and convenience features (rooms, namespaces, auto-reconnection). It's useful if you need cross-browser support for very old clients (IE 9) or need easy room-based broadcasting. For modern apps, raw WebSocket or Supabase Realtime is simpler. Socket.IO adds overhead and requires a server — not worth it for most SaaS.

The Bottom Line

Pick the abstraction that matches your problem. Supabase Realtime for 95% of SaaS (dashboards, notifications, collaborative editing) — it's managed, RLS-scoped, and costs almost nothing. SSE for one-way server-to-client streaming (like AI tokens or price tickers) — it's trivial to implement and works with any backend. Raw WebSocket only when you control both client and server, need microsecond latency, or are building a multiplayer game. For deep Realtime patterns and database-driven subscriptions, see Supabase Realtime — Broadcast, Presence, and DB Changes. Ready to ship? Start with Supabase, measure latency, and iterate. And check Aidxn Design pricing for Realtime scaling numbers on your tier.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.