Skip to content
Back to course
Lesson 10 of 23 · Extra

Polling vs Long-Polling vs SSE vs WebSockets vs Webhooks

Decision guide for picking the right real-time pattern. Five options, each right for a different niche; the wrong one means missed updates, wasted bandwidth, or operational pain.

intermediate8 min read3 diagramsUpdated 11 May 2026✓ Completed
On this page

TL;DR. “I need real-time” is five different problems. The decision pivots on three axes: who pushes (client pull vs server push), connection lifetime (request-scoped vs persistent), and who the receiver is (browser vs server). Default to SSE for server-to-client streaming, WebSockets for bidirectional, and webhooks for server-to-server. Short polling is the fallback when nothing else traverses the network. A WebSocket frame adds 2 to 14 bytes of overhead1; an HTTP request adds 700+2. At scale, that ratio drives architecture.

Learning Objectives

  • Compare five real-time patterns across latency, overhead, directionality, and operational complexity.
  • Identify the workload characteristics (direction, frequency, receiver type) that select each pattern.
  • Justify a hybrid approach layering SSE or WebSockets with webhook delivery for different consumer types.
  • Evaluate production systems (Discord, Stripe, OpenAI) and explain why each chose its pattern.

The Core Trade-off

Every real-time system answers three questions: who initiates the data flow, how long the connection lives, and whether the receiver is a browser or a server. Get any axis wrong and you pay in either missed updates, wasted bandwidth, or operational complexity you did not budget for.

Short polling pulls on a timer. Long polling holds the pull open until data arrives. SSE streams one-way over HTTP. WebSockets upgrade to full-duplex framing. Webhooks POST events between servers.2341

The per-message cost difference is dramatic. A typical HTTP/1.1 request carries 700 to 800 bytes of headers (several KB with cookies)2. A WebSocket frame header is 2 to 14 bytes1. For a 20-byte chat message, short polling has a 35:1 overhead ratio; WebSocket has a 1.1:1 ratio. At millions of messages per second, that math is the architecture.

Diagram
Per-event network cost: short polling pays full HTTP overhead on every check; WebSocket amortizes one handshake across thousands of frames.
sequenceDiagram
    participant C as Client
    participant S as Server
    Note over C,S: Short polling (10s interval, 95% empty)
    C->>S: GET /events (700 B headers)
    S-->>C: 200 [] (400 B)
    C->>S: GET /events (700 B headers)
    S-->>C: 200 [event] (500 B)
    Note over C,S: Long polling (held until data)
    C->>S: GET /events (700 B headers)
    Note right of S: hold 0-30s
    S-->>C: 200 [event] (500 B)
    C->>S: GET /events (reconnect)
    Note over C,S: WebSocket (one handshake, many frames)
    C->>S: Upgrade: websocket
    S-->>C: 101 Switching Protocols
    S-->>C: frame (2-14 B + payload)
    S-->>C: frame (2-14 B + payload)

Side-by-Side Comparison

DimensionShort PollingLong PollingSSEWebSocketsWebhooks
DirectionClient pullClient pull (server holds)Server pushBidirectionalServer-to-server push
Latencyinterval/2 average~1 RTT after event~0 (streaming)~0 (streaming)Delivery + retry backoff
Per-msg overhead~700 B headers2~700 B per event~50 B (field prefix)2-14 B frame1Full HTTP POST
ReconnectN/A (stateless)Immediate re-GETAuto (Last-Event-ID)5Application-levelN/A (stateless)
Proxy compatibilityPerfectGoodGood (HTTP)Requires Upgrade supportPerfect
Connection stateNoneServer holds N FDsServer holds N streamsServer holds N socketsNone
Browser supportUniversalUniversalEventSource API5WebSocket API1N/A (server receiver)
Scale ceilingBandwidth-boundFD/coroutine-boundHTTP/2 stream-bound6FD + memory-boundHTTP worker-bound

The table misleads on “latency.” Long polling delivers in one RTT after the event fires, but each delivery costs a full reconnect cycle. SSE and WebSockets deliver with zero reconnect cost, but demand persistent connections. The dimension that dominates in practice is who the receiver is: browsers cannot receive webhooks; servers should not hold WebSocket connections open for rare events.

When to Pick Polling/SSE

Short polling wins when:

  • Changes are infrequent (minutes apart) and the client is behind a corporate proxy that kills long connections. Email inbox checks, CI build status, DNS propagation monitors.
  • Implementation speed matters more than efficiency. A setInterval + fetch is two lines of code.
  • You need zero server-side connection state. Load balancing is trivial round-robin.

Long polling wins when:

  • You need sub-second detection latency but WebSocket upgrades are blocked. Facebook built its 2008 chat (serving 70 million users) on long polling with Erlang, chosen for its proven concurrency (Yaws benchmarked at 80,000+ parallel connections)4.
  • Failover is free: a dropped connection is indistinguishable from a normal reconnect.

SSE wins when:

  • The flow is server-to-client only. LLM token streaming (OpenAI uses SSE with stream=true)7, stock tickers, progress bars, log tailing.
  • You want built-in reconnect with resume. Last-Event-ID lets the server replay missed events without application-level sequence tracking5.
  • HTTP/2 multiplexing lifts the 6-connection-per-origin limit of HTTP/1.16.

When to Pick WebSockets

WebSockets win when bidirectional, low-overhead, persistent communication is a hard requirement:

  • Chat and messaging. Typing indicators, read receipts, and message delivery all flow client-to-server and server-to-client on the same connection. Discord holds nearly 5 million concurrent WebSocket users on Elixir8.
  • Multiplayer games and collaborative editing. Cursor positions, operational transforms, and game state deltas need sub-10ms framing in both directions.
  • WebRTC signaling. The signaling channel must relay SDP offers and ICE candidates bidirectionally before the peer connection opens.
  • High-frequency bidirectional telemetry. Trading platforms where the client sends orders and the server pushes fills on the same socket.

The cost: WebSocket connections are stateful. You need sticky routing, a pub/sub backplane (Redis, Kafka, NATS) so any gateway can push to any user, and application-level reconnect with sequence replay89. SSE gives you reconnect for free; WebSocket makes you build it.

The Hybrid Path

Most production systems combine patterns by receiver type. Slack’s architecture is the canonical example: the legacy Real Time Messaging API was a single WebSocket connection per app per workspace. Slack deprecated it and replaced it with the Events API, which delivers over HTTP webhooks for server integrations and “Socket Mode” (a Slack-initiated WebSocket) for apps behind firewalls1011.

The pattern: browsers get WebSockets or SSE for low-latency push; partner servers get webhooks for decoupled, retry-safe delivery; internal services get event streams via Kafka or NATS.

Diagram
Hybrid delivery: one event source fans out through three transport layers matched to each consumer type.
flowchart LR
    subgraph Producers
        SVC[Application Service]
    end
    subgraph "Delivery layer"
        GW[WebSocket Gateway]
        SSE_EP[SSE Endpoint]
        WH[Webhook Dispatcher]
    end
    subgraph Consumers
        Browser[Browser / Mobile]
        LLM[LLM UI Client]
        Partner[Partner Server]
    end
    SVC --> GW
    SVC --> SSE_EP
    SVC --> WH
    GW -->|bidirectional frames| Browser
    SSE_EP -->|text/event-stream| LLM
    WH -->|signed POST + retries| Partner

Real-World Examples

Discord (WebSockets at scale): Nearly 5 million concurrent WebSocket users in 20178, scaling to 11 million by 201912. Each session is an Elixir GenServer; fanout uses Manifold to group target PIDs by node and shard within nodes. Before Manifold, publishing to a 30,000-member guild took 900 ms to 2.1 s; after, latency dropped dramatically8. WhatsApp pushed further: 2 million TCP connections per FreeBSD server in 201213.

Stripe (Webhooks): HMAC-SHA256 over timestamp + "." + body with a 5-minute replay window1415. At-least-once delivery with exponential backoff retries for up to 3 days14. Receivers must be idempotent on event.id because duplicates are guaranteed.

OpenAI (SSE): Chat Completions and Responses APIs stream tokens via Content-Type: text/event-stream7. Each data: frame carries a JSON delta. The client renders tokens as they arrive instead of waiting for full completion. SSE’s built-in reconnect and Last-Event-ID make it simpler than WebSocket for this unidirectional use case.

Common Mistakes

Decision Checklist

  • Is the receiver a browser/app or a server? (Server = webhooks; browser = SSE or WebSocket.)
  • Does data flow bidirectionally or server-to-client only? (Bidirectional = WebSocket.)
  • What is the acceptable detection latency? (Minutes = polling; sub-second = SSE/WS/long-poll.)
  • Can you hold persistent connections? (If not, polling or webhooks.)
  • Is the receiver always online or intermittent? (Intermittent = webhooks with retries and DLQ.)
  • Does your infrastructure support HTTP Upgrade? (If blocked, SSE or long polling.)
  • What is your message frequency? (Rare = webhooks/polling; continuous = SSE/WebSocket.)
Diagram
Decision flowchart: start with receiver type, then directionality, then latency requirement. Each leaf is a defensible default.
flowchart TD
    Start["Need real-time updates"] --> Q1{"Receiver is a server?"}
    Q1 -- yes --> WH["Webhooks<br/>HMAC + retries + DLQ"]
    Q1 -- no --> Q2{"Bidirectional needed?"}
    Q2 -- yes --> WS["WebSockets<br/>RFC 6455, sticky routing"]
    Q2 -- no --> Q3{"Persistent stream<br/>of server events?"}
    Q3 -- yes --> SSE["SSE<br/>auto-reconnect, Last-Event-ID"]
    Q3 -- no --> Q4{"Sub-second latency<br/>required?"}
    Q4 -- yes --> LP["Long polling<br/>HTTP-compatible fallback"]
    Q4 -- no --> SP["Short polling<br/>simplest, interval-based"]

Key Takeaways

  • The decision pivots on three axes: direction (pull vs push), connection lifetime (ephemeral vs persistent), and receiver type (browser vs server).
  • WebSocket frame overhead is 2-14 bytes; HTTP request overhead is 700+ bytes. At high message rates, this ratio dominates architecture.21
  • SSE is the correct default for server-to-client streaming. It gives you auto-reconnect and resume for free; WebSocket makes you build both.
  • Webhooks are the correct default for server-to-server. No persistent connection, industry-standard retry patterns, and horizontal scaling via stateless HTTP workers.
  • Most production systems at scale use a hybrid: WebSocket/SSE for browsers, webhooks for partners, event streams for internal services.

Further Reading

Flashcards

What is the per-message overhead of a WebSocket frame vs an HTTP request?

A WebSocket frame adds 2 to 14 bytes of header. A typical HTTP/1.1 request adds 700 to 800 bytes of headers (more with cookies). For small payloads, HTTP has a 35:1+ overhead ratio.

When should you pick SSE over WebSockets?

When data flows server-to-client only. SSE gives you auto-reconnect with Last-Event-ID resume for free, works over HTTP/1.1 and HTTP/2, and requires no application-level heartbeat or replay logic. Use it for LLM streaming, stock tickers, and progress updates.

Why did Slack deprecate its RTM WebSocket API?

The Events API (webhooks) lets partner integrations scale with stateless HTTP workers and requires no sticky routing or persistent connections. Socket Mode remains as an escape hatch for firewalled apps.

What three things must a webhook receiver do?

(1) Verify the HMAC signature against the raw body bytes. (2) Return 2xx within the provider’s timeout (Stripe: fast; GitHub: 10 seconds; Slack: 3 seconds). (3) Process idempotently, deduplicating on event ID, because at-least-once delivery guarantees duplicates.

How does Discord handle WebSocket reconnection without losing events?

Each dispatch carries a monotonic sequence number (s). On reconnect, the client sends op 6 Resume with the last s value, and the server replays missed dispatches. This is the application-level equivalent of SSE’s Last-Event-ID.

What is the main operational cost of WebSockets at scale?

Statefulness. Each connection pins a user to a gateway process, requiring sticky routing, a pub/sub backplane for cross-gateway fanout, connection-aware autoscaling, and application-level reconnect with auth re-handshake.

How many concurrent WebSocket connections did Discord and WhatsApp achieve?

Discord held 5 million concurrent users on Elixir (2017), scaling to 11 million by 2019. WhatsApp achieved 2 million TCP connections per FreeBSD server in 2012 by tuning the Erlang VM and OS kernel.

References

Footnotes

  1. IETF, “The WebSocket Protocol”, RFC 6455, 2011. https://datatracker.ietf.org/doc/html/rfc6455 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. “WebSockets Protocol vs HTTP”, Stack Overflow. https://stackoverflow.com/questions/14703627/WebSockets-Protocol-VS-http ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  3. Karan Pratap Singh, “System Design: Long polling, WebSockets, Server-Sent Events”. https://dev.to/karanpratapsingh/system-design-long-polling-websockets-server-sent-events-sse-1hip ↩ ↩2

  4. Todd Hoff, “New Facebook Chat Feature Scales to 70 Million Users Using Erlang”, High Scalability, 2008. https://highscalability.com/new-facebook-chat-feature-scales-to-70-million-users-using-e/ ↩ ↩2

  5. WHATWG, “Server-sent events”, HTML Living Standard. https://html.spec.whatwg.org/multipage/server-sent-events.html ↩ ↩2 ↩3 ↩4

  6. Better-SSE FAQ, “Browser connection limits for HTTP/1.1 vs HTTP/2”. https://matthewwid.github.io/better-sse/reference/faq/ ↩ ↩2

  7. OpenAI, “Streaming API responses”. https://developers.openai.com/api/docs/guides/streaming-responses ↩ ↩2

  8. Stanislav Vishnevskiy, “How Discord Scaled Elixir to 5,000,000 Concurrent Users”, Discord Engineering, 2017. https://discord.com/blog/how-discord-scaled-elixir-to-5-000-000-concurrent-users ↩ ↩2 ↩3 ↩4 ↩5

  9. Discord Developer Documentation, “Gateway”. https://discord.com/developers/docs/events/gateway ↩

  10. Slack, “Real Time Messaging (RTM)”. https://docs.slack.dev/tools/node-slack-sdk/rtm-api/ ↩

  11. Slack, “The Events API”. https://docs.slack.dev/apis/events-api/ ↩

  12. Matt Nowack, “Using Rust to Scale Elixir for 11 Million Concurrent Users”, Discord Engineering, 2019. https://discord.com/blog/using-rust-to-scale-elixir-for-11-million-concurrent-users ↩

  13. Todd Hoff, “The WhatsApp Architecture Facebook Bought For $19 Billion”, High Scalability, 2014. https://highscalability.com/the-whatsapp-architecture-facebook-bought-for-19-billion/ ↩

  14. Stripe Docs, “Receive Stripe events in your webhook endpoint”. https://docs.stripe.com/webhooks ↩ ↩2 ↩3

  15. Stripe Docs, “Resolve webhook signature verification errors”. https://docs.stripe.com/webhooks/signature ↩ ↩2

Finished reading?

Mark it complete to track your progress through the course. Progress is saved in this browser.

This lesson is adapted from “Polling vs Long-Polling vs SSE vs WebSockets vs Webhooks” in The Engineering Handbook by Handbook Academy and contributors, used under CC BY-SA 4.0. Changes: restyled and reorganized for this site. View original source.