Skip to main content
โšก Calmops

Real-Time Web Technologies: WebSockets, SSE, and Real-Time Databases Explained

Introduction: The Rise of Real-Time Web Applications

Real-time web applications have become essential in modern software development. Whether it’s collaborative editing in Google Docs, live notifications on social media, or real-time data analytics dashboards, users expect instant updates without page refreshes. But implementing real-time features isn’t one-size-fits-all. Choosing the wrong technology can lead to scalability issues, unnecessary complexity, or poor user experience.

In this guide, we’ll explore three primary approaches to building real-time web applications: WebSockets for bidirectional communication, Server-Sent Events (SSE) for server-to-client streaming, and real-time databases like Firebase and Supabase that abstract away the complexity. By the end, you’ll understand the trade-offs and be equipped to make informed architectural decisions.


Understanding Real-Time Web Communication

Before diving into specific technologies, let’s clarify what “real-time” means in web development:

  • Real-time: Data is transmitted from server to client (and sometimes back) with minimal latencyโ€”typically under 1-2 seconds
  • Bidirectional: Both client and server can initiate communication
  • Unidirectional: Only one party initiates communication

Traditional HTTP requests are stateless and initiated by the client. Real-time technologies overcome this limitation by maintaining persistent connections or enabling server-initiated communication.


WebSockets: Bidirectional Communication Over TCP

What Are WebSockets?

WebSockets establish a persistent, bidirectional TCP connection between client and server. Unlike HTTP, which creates a new connection for each request, a WebSocket connection remains open, allowing either party to send data at any time.

Key Characteristics

  • Bidirectional: Both client and server can send messages
  • Low latency: No HTTP overhead per message
  • Stateful: Connection persists across messages
  • Full-duplex: Simultaneous two-way communication
  • Browser support: Excellent (99%+ of modern browsers)

When to Use WebSockets

  • Real-time multiplayer games
  • Live collaboration tools (shared documents, whiteboards)
  • Chat applications
  • Live trading platforms
  • Real-time notifications with user interaction
  • Streaming data (sensor data, live metrics)

WebSocket Implementation Example

Here’s a basic example using Node.js with the ws library:

Server (Node.js/Express):

const express = require('express');
const WebSocket = require('ws');
const http = require('http');

const app = express();
const server = http.createServer(app);

// Create a WebSocket server attached to HTTP server
const wss = new WebSocket.Server({ server });

// Handle new connections
wss.on('connection', (ws) => {
  console.log('Client connected');

  // Handle incoming messages from clients
  ws.on('message', (data) => {
    console.log('Received:', data);
    
    // Echo message to all connected clients (broadcast)
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({
          type: 'message',
          payload: data,
          timestamp: new Date().toISOString()
        }));
      }
    });
  });

  // Handle client disconnect
  ws.on('close', () => {
    console.log('Client disconnected');
  });

  // Send initial connection confirmation
  ws.send(JSON.stringify({ type: 'connected', message: 'Welcome!' }));
});

server.listen(3000, () => console.log('Server on port 3000'));

Client (React/TypeScript):

import { useEffect, useRef, useState } from 'react';

export function ChatComponent() {
  const [messages, setMessages] = useState<string[]>([]);
  const [input, setInput] = useState('');
  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    // Establish WebSocket connection
    wsRef.current = new WebSocket('ws://localhost:3000');

    wsRef.current.onopen = () => {
      console.log('Connected to server');
    };

    wsRef.current.onmessage = (event) => {
      // Receive messages from server
      const data = JSON.parse(event.data);
      if (data.type === 'message') {
        setMessages((prev) => [...prev, data.payload]);
      }
    };

    wsRef.current.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    // Cleanup on component unmount
    return () => {
      if (wsRef.current) {
        wsRef.current.close();
      }
    };
  }, []);

  const sendMessage = () => {
    if (wsRef.current && input) {
      wsRef.current.send(input);
      setInput('');
    }
  };

  return (
    <div>
      <div className="messages">
        {messages.map((msg, i) => (
          <p key={i}>{msg}</p>
        ))}
      </div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
        placeholder="Type a message..."
      />
      <button onClick={sendMessage}>Send</button>
    </div>
  );
}

WebSocket Advantages

  • Low latency: Minimal protocol overhead
  • Efficient: No redundant HTTP headers per message
  • True bidirectionality: Both parties can initiate communication
  • Well-supported: Native browser API, extensive library ecosystem

WebSocket Challenges

  • Server complexity: Requires stateful server architecture
  • Scaling complexity: Load balancing requires sticky sessions or shared state
  • Connection management: Reconnection logic needed for reliability
  • Memory overhead: Open connections consume server resources

Server-Sent Events (SSE): Server-to-Client Streaming

What Is SSE?

Server-Sent Events (SSE) is a simpler alternative to WebSockets for scenarios where the server primarily sends updates to clients. It uses HTTP’s long-polling mechanism to maintain a persistent connection for server-initiated messages.

Key Characteristics

  • Unidirectional: Server sends to client (client communication requires separate HTTP)
  • HTTP-based: Uses standard HTTP connections, easier to scale
  • Text-based: Messages are UTF-8 text (or JSON)
  • Automatic reconnection: Built-in retry logic
  • Browser support: Excellent (98%+ of modern browsers)

When to Use SSE

  • Live notifications and alerts
  • Real-time data feeds (news, stock prices)
  • Server-sent logs and monitoring dashboards
  • Push notifications from server
  • Live progress updates (file uploads, batch processing)

SSE Implementation Example

Server (Node.js/Express):

const express = require('express');
const app = express();

// SSE endpoint for streaming updates
app.get('/events', (req, res) => {
  // Set SSE headers
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('Access-Control-Allow-Origin', '*');

  // Send initial message
  res.write('data: {"type": "connected", "message": "Stream started"}\n\n');

  // Simulate sending updates every 5 seconds
  const intervalId = setInterval(() => {
    const event = {
      type: 'update',
      timestamp: new Date().toISOString(),
      data: Math.random()
    };
    
    // SSE format: "data: <message>\n\n"
    res.write(`data: ${JSON.stringify(event)}\n\n`);
  }, 5000);

  // Clean up interval when client disconnects
  req.on('close', () => {
    clearInterval(intervalId);
    res.end();
  });
});

app.listen(3000, () => console.log('SSE server on port 3000'));

Client (React/TypeScript):

import { useEffect, useState } from 'react';

export function NotificationDashboard() {
  const [notifications, setNotifications] = useState<any[]>([]);

  useEffect(() => {
    // Create EventSource connection to SSE endpoint
    const eventSource = new EventSource('http://localhost:3000/events');

    eventSource.onmessage = (event) => {
      // Parse incoming SSE message
      const data = JSON.parse(event.data);
      console.log('Received update:', data);
      
      setNotifications((prev) => [...prev, data]);
    };

    eventSource.onerror = (error) => {
      console.error('EventSource error:', error);
      // EventSource will automatically attempt to reconnect
      if (eventSource.readyState === EventSource.CLOSED) {
        console.log('Connection closed');
      }
    };

    // Cleanup on component unmount
    return () => {
      eventSource.close();
    };
  }, []);

  return (
    <div>
      <h2>Live Notifications</h2>
      <ul>
        {notifications.map((notif, i) => (
          <li key={i}>
            {notif.type}: {notif.data?.toFixed(2)} at {new Date(notif.timestamp).toLocaleTimeString()}
          </li>
        ))}
      </ul>
    </div>
  );
}

SSE Advantages

  • Simplicity: Easier to implement than WebSockets
  • Scalability: Works well with standard HTTP load balancers
  • Automatic reconnection: Built-in resilience
  • Server-controlled: No client-initiated noise

SSE Limitations

  • Unidirectional: Client can’t easily send data through the same connection
  • Text-only: Binary data requires encoding
  • No multiplexing: One connection per stream
  • HTTP limitations: Proxy and firewall behavior can affect connections

Real-Time Databases: Firebase and Supabase

What Are Real-Time Databases?

Real-time databases are managed backend services that handle real-time data synchronization across clients automatically. They abstract away the complexity of WebSockets or SSE by providing high-level APIs for subscribing to data changes.

Firebase Realtime Database

Firebase is Google’s managed platform offering real-time database capabilities with built-in authentication and hosting.

Firebase Implementation Example:

import { initializeApp } from 'firebase/app';
import { 
  getDatabase, 
  ref, 
  onValue, 
  set, 
  push 
} from 'firebase/database';

// Initialize Firebase
const firebaseConfig = {
  apiKey: 'YOUR_API_KEY',
  authDomain: 'YOUR_PROJECT.firebaseapp.com',
  databaseURL: 'https://YOUR_PROJECT.firebaseio.com',
  projectId: 'YOUR_PROJECT',
};

const app = initializeApp(firebaseConfig);
const db = getDatabase(app);

// Component for real-time collaboration
export function CollaborativeEditor() {
  const [content, setContent] = React.useState('');

  React.useEffect(() => {
    // Create reference to "documents/doc1" in Firebase
    const docRef = ref(db, 'documents/doc1');

    // Subscribe to real-time updates
    const unsubscribe = onValue(docRef, (snapshot) => {
      if (snapshot.exists()) {
        setContent(snapshot.val().text);
      }
    });

    return unsubscribe; // Cleanup subscription
  }, []);

  const updateContent = (newText: string) => {
    setContent(newText);
    
    // Write changes back to Firebase
    set(ref(db, 'documents/doc1'), {
      text: newText,
      lastModified: new Date().toISOString()
    });
  };

  return (
    <textarea
      value={content}
      onChange={(e) => updateContent(e.target.value)}
      placeholder="Start typing..."
    />
  );
}

Supabase: PostgreSQL with Real-Time

Supabase is an open-source Firebase alternative built on PostgreSQL, offering real-time capabilities through WebSockets under the hood.

Supabase Implementation Example:

import { createClient } from '@supabase/supabase-js';

// Initialize Supabase client
const supabase = createClient(
  'https://YOUR_PROJECT.supabase.co',
  'YOUR_ANON_KEY'
);

export function RealtimeMessages() {
  const [messages, setMessages] = React.useState<any[]>([]);

  React.useEffect(() => {
    // Subscribe to changes in the "messages" table
    const subscription = supabase
      .from('messages')
      .on('*', (payload) => {
        console.log('Change received!', payload);
        
        if (payload.eventType === 'INSERT') {
          setMessages((prev) => [...prev, payload.new]);
        } else if (payload.eventType === 'UPDATE') {
          setMessages((prev) =>
            prev.map((msg) =>
              msg.id === payload.new.id ? payload.new : msg
            )
          );
        } else if (payload.eventType === 'DELETE') {
          setMessages((prev) =>
            prev.filter((msg) => msg.id !== payload.old.id)
          );
        }
      })
      .subscribe();

    // Fetch initial messages
    supabase
      .from('messages')
      .select('*')
      .then(({ data }) => setMessages(data || []));

    return () => {
      supabase.removeSubscription(subscription);
    };
  }, []);

  const addMessage = async (text: string) => {
    await supabase.from('messages').insert([
      {
        text,
        created_at: new Date().toISOString(),
        user_id: 'current_user_id' // From authentication
      }
    ]);
  };

  return (
    <div>
      <h2>Messages</h2>
      <ul>
        {messages.map((msg) => (
          <li key={msg.id}>{msg.text}</li>
        ))}
      </ul>
      <input
        onKeyPress={(e) => {
          if (e.key === 'Enter') {
            addMessage(e.currentTarget.value);
            e.currentTarget.value = '';
          }
        }}
        placeholder="Type a message..."
      />
    </div>
  );
}

Real-Time Database Advantages

  • Managed: No server infrastructure to maintain
  • Built-in authentication: Integrated user management
  • High abstraction: Simple APIs hide complexity
  • Scalable: Automatically handled by provider
  • Security rules: Declarative access control

Real-Time Database Limitations

  • Vendor lock-in: Difficult to migrate away
  • Cost: Pay-as-you-go pricing can exceed self-hosted solutions
  • Limited flexibility: Constraints in what you can customize
  • Potential latency: Going through vendor’s infrastructure

Technology Comparison

Here’s a comprehensive comparison to help guide your decision:

Aspect WebSockets SSE Firebase Supabase
Communication Bidirectional Unidirectional Bidirectional Bidirectional
Complexity High Low Low Low-Medium
Browser Support 99%+ 98%+ 99%+ 99%+
Setup Time Days Hours Minutes Hours
Scalability Medium* High High High
Cost Infrastructure Infrastructure Pay-as-you-go Pay-as-you-go
Self-hosted Yes Yes No Yes
Learning Curve Steep Gentle Gentle Medium
Real-time Latency <100ms <1s <500ms <500ms
Best For Games, Collaboration Notifications, Feeds Mobile/Rapid Dev Full-stack Apps

*WebSockets require sticky sessions or shared state for horizontal scaling


Choosing the Right Technology

Use WebSockets When

  • You need true bidirectional real-time communication
  • Building multiplayer games or collaborative apps
  • You have complex, frequent interactions between client and server
  • You need low latency and control over the protocol

Use SSE When

  • Server primarily sends updates to clients
  • You need simple server-push notifications
  • You want scalability without sticky sessions
  • Building real-time dashboards or data feeds

Use Firebase When

  • You want rapid prototyping without backend infrastructure
  • Building mobile apps with real-time sync
  • You prioritize time-to-market over customization
  • You’re comfortable with vendor services

Use Supabase When

  • You need PostgreSQL power with real-time features
  • You want self-hosting flexibility while keeping managed convenience
  • Building full-stack applications with complex data models
  • You prefer open-source solutions with portability

Performance and Scalability Considerations

Connection Management

  • WebSockets: Each connection consumes server memory. Use connection pooling and implement heartbeat mechanisms
  • SSE: More efficient per-connection but can create proxy compatibility issues
  • Real-time Databases: Provider handles scaling; you pay for throughput

Message Volume

  • WebSockets: Best for high-frequency bidirectional traffic
  • SSE: Suitable for one-way streaming; add HTTP POST for responses
  • Real-time Databases: Designed for moderate to high volume with automatic scaling

Horizontal Scaling

  • WebSockets: Requires sticky sessions or message broker (Redis) for state sharing
  • SSE: Naturally scales horizontally with standard load balancers
  • Real-time Databases: Automatically scaled by provider

Best Practices and Common Pitfalls

Best Practices

  1. Implement reconnection logic: Handle network failures gracefully
  2. Use message compression: Reduce bandwidth for large payloads
  3. Set connection timeouts: Prevent zombie connections
  4. Monitor latency: Track end-to-end message delivery time
  5. Implement rate limiting: Prevent client-side spam
  6. Use heartbeats: Keep connections alive through proxies
  7. Log connection events: Debug production issues effectively

Common Pitfalls

  • Ignoring network unreliability: Assume connections will fail
  • Broadcasting all messages: Filter and route messages efficiently
  • Storing too much in-memory: Use databases for persistent state
  • Forgetting to cleanup subscriptions: Prevent memory leaks
  • Over-engineering simple features: SSE often suffices for notifications
  • Ignoring browser WebSocket limits: Most browsers limit ~6 concurrent connections

Hybrid Approaches

Modern applications often combine multiple technologies:

  • WebSocket + SSE: Use WebSockets for bidirectional chat, SSE for notifications
  • Real-time Database + WebSockets: Use Supabase for persistence, WebSockets for low-latency trading
  • GraphQL Subscriptions: Build on WebSockets with GraphQL for type-safe real-time APIs
// Example: GraphQL Subscriptions (combines WebSocket + type safety)
import { gql, useSubscription } from '@apollo/client';

const MESSAGE_SUBSCRIPTION = gql`
  subscription OnMessageAdded {
    messageAdded {
      id
      content
      user { name }
      createdAt
    }
  }
`;

export function MessagesWithGraphQL() {
  const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION);

  if (loading) return <p>Loading...</p>;

  return (
    <ul>
      {data?.messageAdded && (
        <li>{data.messageAdded.content}</li>
      )}
    </ul>
  );
}

Conclusion

Real-time web technologies have evolved significantly, offering multiple paths to build responsive, interactive applications. WebSockets provide maximum control and minimal latency for bidirectional communication. Server-Sent Events offer simplicity and excellent scalability for server-to-client streams. Managed real-time databases like Firebase and Supabase accelerate development with minimal infrastructure overhead.

The “best” choice depends on your specific requirements:

  • Need bidirectional, low-latency communication? โ†’ WebSockets
  • Building simple notifications or data feeds? โ†’ SSE
  • Rapid prototyping without backend infrastructure? โ†’ Firebase
  • Full-stack app with PostgreSQL and flexibility? โ†’ Supabase

Start with the simplest solution that meets your requirements. You can always migrate to a more complex architecture as your needs evolve. Many successful applications use multiple real-time technologies in conjunction, each solving specific problems.

By understanding these technologies’ trade-offs, you’re now equipped to make informed decisions and build the responsive, real-time experiences users expect.


Further Reading

Comments