Skip to content
ExponaBLOG
KAFKA & WEBSOCKETS · DAY 051 / 100

WebSockets vs SSE vs polling: what developers should know

Pick the simplest option that works.

Expona Admin8 min read

WebSockets vs SSE vs polling

After fifty days of building real-time systems, I've learned that the worst decision isn't picking the wrong technology—it's picking an over-engineered one. When you're deciding between WebSockets vs SSE vs polling, you're really asking: how much complexity can I afford, and how much latency can I tolerate? Let me walk you through each option with the scenarios where I've actually used them in production, so you can make the right call for your next feature.

Polling: The Simplest Path That Works

Polling is your friend when you don't know if you need real-time yet. The client asks the server "anything new?" on a fixed interval. No WebSocket connection. No server-sent events. Just HTTP requests, usually with a GET endpoint that returns the latest state.

I've deployed polling successfully in several production systems. One was an internal SaaS dashboard where users checked order status every few minutes. Another was a ride-hailing mobile app that updated driver location every 5–10 seconds. The beauty is that it works everywhere: old browsers, restrictive corporate firewalls, load balancers that don't understand WebSockets.

Here's the pattern I use:

typescript

// Client: React component polling for order updates
useEffect(() => {
  const interval = setInterval(async () => {
    const response = await fetch(`/api/orders/${orderId}`);
    const order = await response.json();
    setOrder(order);
  }, 5000); // Poll every 5 seconds

  return () => clearInterval(interval);
}, [orderId]);

The server side is trivial:

typescript

// NestJS endpoint
@Get('orders/:id')
async getOrder(@Param('id') id: string) {
  return this.ordersService.findOne(id);
}

The trade-off is obvious: latency. If something changes, the client won't know until the next poll. For an order dashboard, a 5-second delay is fine. For a live trading system, it's unacceptable. Also, if you have 10,000 concurrent users polling every 5 seconds, your server will see 2,000 requests per second. That's workable but not elegant.

When to use polling: Infrequent updates (every few seconds or slower), high client count with bursty traffic, or when you're not sure if you need real-time yet.

Long Polling: The Legacy Fallback You'll Eventually Need

Long polling is polling's clever cousin. The client makes a request, but the server doesn't respond immediately. It holds the connection open until new data arrives or a timeout occurs (usually 20–60 seconds). Then the client immediately opens a new long poll.

I've used this as a fallback in production when WebSocket support was uncertain. It reduced the gap between regular polling and true real-time while staying compatible with older infrastructure.

On the client:

typescript

// React hook for long polling
async function longPoll(endpoint: string) {
  while (true) {
    try {
      const response = await fetch(endpoint, {
        method: 'GET',
        signal: AbortSignal.timeout(60000), // 60-second timeout
      });
      if (response.ok) {
        const data = await response.json();
        if (data) {
          // Process data, then immediately poll again
          handleData(data);
        }
      }
    } catch (err) {
      if (err.name !== 'AbortError') console.error(err);
      // Retry after a short delay
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
}

On the server (NestJS with Kafka), I'd buffer messages and return them when available:

typescript

@Get('events/:userId')
async pollEvents(
  @Param('userId') userId: string,
  @Res() res: Response,
) {
  // Wait up to 50 seconds for an event
  const event = await this.eventService.waitForEvent(userId, 50000);
  if (event) {
    res.json(event);
  } else {
    res.status(204).send(); // No content
  }
}

Long polling cuts wasted requests compared to short polling and feels closer to real-time. The downside is connection churn—you're opening and closing connections constantly, which adds overhead.

When to use long polling: When you need something faster than regular polling but can't rely on WebSocket support (older browsers, highly restrictive network environments).

SSE: Server-to-Client Stream Over HTTP

Server-Sent Events (SSE) is the underrated middle ground. The client opens a persistent HTTP connection, and the server pushes data whenever it's ready. It's unidirectional: server talks to client only.

I used SSE in a trading platform where we needed to stream live price ticks to multiple clients. The server publishes events to Kafka, a consumer reads them, and SSE connections receive the updates in real time. It was simpler than managing WebSocket lifecycle events and worked reliably through our load balancers.

Client-side:

typescript

// React hook for SSE
useEffect(() => {
  const eventSource = new EventSource(`/api/prices/${symbolId}/stream`);

  eventSource.onmessage = (event) => {
    const price = JSON.parse(event.data);
    setPrices(prev => ({ ...prev, [symbolId]: price }));
  };

  eventSource.onerror = () => {
    eventSource.close();
    // Reconnect after delay
    setTimeout(() => window.location.reload(), 5000);
  };

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

Server-side (NestJS streaming with Kafka):

typescript

@Sse('prices/:id/stream')
priceStream(@Param('id') symbolId: string) {
  return this.pricesService.streamPrices(symbolId).pipe(
    map(price => ({ data: JSON.stringify(price) })),
  );
}

// In the service, using Kafka
streamPrices(symbolId: string) {
  return new Observable(observer => {
    this.kafkaService.subscribe(`prices.${symbolId}`, message => {
      observer.next(message);
    });
  });
}

SSE is simple, uses standard HTTP, and browsers handle reconnection automatically if the connection drops. The catch: it's one-way. If your client needs to send complex messages to the server, you'll need SSE plus an HTTP POST endpoint or a separate WebSocket channel.

When to use SSE: Server-to-client push only, moderate latency requirements (sub-second is fine), and you want to avoid WebSocket complexity.

WebSockets: Bidirectional, Low Latency

WebSockets open a persistent TCP connection that both client and server can write to. It's the lowest-latency option and fully bidirectional. I use it when I need real-time, two-way communication: collaborative editing, live trading, multiplayer games, or chat.

In a ride-hailing platform, I used WebSockets to stream driver location updates to passengers and to accept ride accept/decline decisions from drivers in near-real time. The latency mattered.

Client-side (with reconnection logic):

typescript

// React hook for WebSocket
useEffect(() => {
  let socket: WebSocket;
  let reconnectTimeout: NodeJS.Timeout;

  function connect() {
    socket = new WebSocket(`wss://${window.location.host}/rides/${rideId}`);

    socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      if (message.type === 'location_update') {
        setDriverLocation(message.payload);
      }
    };

    socket.onerror = () => socket.close();
    socket.onclose = () => {
      reconnectTimeout = setTimeout(connect, 3000);
    };
  }

  connect();

  return () => {
    socket?.close();
    clearTimeout(reconnectTimeout);
  };
}, [rideId]);

// Send a message to the server
const acceptRide = () => {
  socket.send(JSON.stringify({ type: 'ride_accepted' }));
};

Server-side (NestJS with @nestjs/websockets):

typescript

@WebSocketGateway({ namespace: 'rides' })
export class RideGateway
  implements OnGatewayConnection, OnGatewayDisconnect {
  private driverLocations = new Map();

  @WebSocketServer() server: Server;

  handleConnection(client: Socket) {
    const rideId = client.handshake.query.rideId;
    client.join(`ride:${rideId}`);
  }

  @SubscribeMessage('location_update')
  handleLocationUpdate(client: Socket, data: any) {
    const rideId = client.handshake.query.rideId;
    this.driverLocations.set(rideId, data.location);
    this.server.to(`ride:${rideId}`).emit('location_update', data.location);
  }

  handleDisconnect(client: Socket) {
    // Cleanup
  }
}

WebSockets scale well if you architect them correctly—partition connections by region, use a message broker (Kafka, Redis) to broadcast across servers, and implement proper backpressure. They're more complex than polling or SSE but worth it when latency is critical.

When to use WebSockets: Real-time, bidirectional communication required; sub-100ms latency is essential; users expect instant feedback.

Common Mistakes

  • Polling everything: I've seen teams poll endpoints every 500ms and wonder why their servers are exhausted. If updates are infrequent, 5–10 second polling is usually fine. If they're frequent, use SSE or WebSockets.
  • WebSockets without backpressure: Pushing thousands of messages per second to 10,000 clients without flow control will crash your server. Always buffer and apply backpressure or drop messages deliberately.
  • Forgetting reconnection logic: Connections will drop. If your client doesn't handle reconnection, users will see stale data or a frozen UI. Use libraries like socket.io or implement exponential backoff.
  • Mixing protocols without a reason: SSE + WebSocket is sometimes necessary, but often it's over-engineering. Pick one and stick with it.
  • Ignoring Kafka or message broker overhead: If you're using Kafka to fan out messages to WebSocket clients, measure end-to-end latency. Kafka adds ~100–200ms in typical setups. For sub-100ms requirements, consider Redis Pub/Sub instead.

Key Takeaways

  • Start with polling if latency isn't critical. It's the simplest and works in almost any environment.
  • SSE is ideal for one-way server-to-client updates and feels more efficient than repeated polling.
  • Long polling bridges the gap when you need something faster than polling but can't rely on WebSocket support.
  • WebSockets are your choice when latency matters and you need two-way communication, but they require thoughtful connection management and proper scaling architecture.
  • Always measure your actual latency requirements before defaulting to WebSockets. You might find that polling or SSE solves your problem at a fraction of the complexity.
  • If you're using Kafka, remember that it's not a low-latency event stream—it's a durable, ordered queue. For real-time pushes, use Redis Pub/Sub or a WebSocket gateway pattern.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles