Skip to content
yutils

Webhooks vs Polling vs SSE vs WebSocket — Picking the Right Real-Time Pattern

Four real-time integration patterns compared on latency, cost, complexity, and failure modes. When to use webhooks, when to fall back to polling, and where SSE/WebSocket fit.

~9 min read

"How do we get Stripe charge completions into our system?" — the answer shapes the architecture. Webhooks, polling, Server-Sent Events (SSE), and WebSocket — all share the goal of "one side notices the other's change," but latency, cost, and complexity differ wildly. This guide compares the four and explains which to pick when.

Four patterns in one table

PatternDirectionLatencyCostTypical use
PollingClient → Server (repeated pull)interval (seconds–minutes)requests/interval × nJob status, fallback for missing webhooks
WebhookServer → Server (event push)near-instantone per eventStripe charges, GitHub push, Slack commands
SSEServer → Client (stream)instantone connection per clientChats, dashboards, one-way live updates
WebSocketBidirectional (full-duplex)instantone connection, both waysCollaboration, games, trading

1. Polling — simple but coarse

The client asks the server at a fixed interval whether anything changed.

setInterval(async () => {
  const res = await fetch("/api/job/123/status");
  const {status} = await res.json();
  if (status === "done") {
    showResult();
    return;
  }
}, 5000);  // every 5 seconds

Pros:

  • Zero infrastructure — plain HTTP. Firewalls and proxies are fine.
  • Recovery = the next poll. Stateless.
  • Five minutes to implement.

Cons:

  • Latency = interval. One-second polling adds 60 req/min/user to origin load.
  • Most responses say "no change" → wasted bandwidth.

Long polling — polling's middle child

The server holds the request open until a change occurs (or timeout). Reduces latency at the cost of holding connections. Mostly displaced by SSE/WebSocket.

2. Webhook — server-to-server push

The provider POSTs to your endpoint when an event happens. The standard for every modern SaaS API — Stripe, GitHub, Slack, etc.

// Receiving endpoint
POST /webhooks/stripe
Stripe-Signature: t=...,v1=...
Content-Type: application/json

{ "type": "charge.succeeded", "data": {...} }

Pros:

  • Near-instant latency.
  • One request per event. No waste.
  • No connection state on the client.

Cons:

  • Needs a public endpoint — localhost and intranet won't work. Use ngrok/cloudflared in development.
  • Authenticating the sender is critical. Signature verification (with constant-time compare via HMAC Verify) is mandatory.
  • You depend on the provider's retry policy. What happens if your server is down briefly?

Webhook retry policies per provider

  • Stripe — up to 3 days, exponential backoff (5 s → 8 h).
  • GitHub — 5 attempts within 8 hours, then disabled.
  • Slack — 5 attempts.

The receiver should return 200 within 5 seconds. Push real work onto a queue. The canonical flow: verify signature → respond 200 → enqueue job.

Signature verification example

// Stripe-style (see [[hmac-webhooks]] for details)
import {createHmac, timingSafeEqual} from "crypto";

function verify(body, sigHeader, secret) {
  const [t, v1] = parseSignature(sigHeader);
  const signed = `${t}.${body}`;
  const expected = createHmac("sha256", secret).update(signed).digest("hex");
  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(v1, "hex"));
}

Compute the same signature yourself in HMAC Generator to make the format concrete.

3. Server-Sent Events — one-way streams

A W3C standard layered on HTTP. The server streams text events; the browser's EventSource handles parsing.

// Server (Node.js)
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");

setInterval(() => {
  res.write(`data: ${JSON.stringify({time: Date.now()})}\n\n`);
}, 1000);

// Client
const es = new EventSource("/api/stream");
es.onmessage = (e) => {
  console.log(JSON.parse(e.data));
};

Pros:

  • Plain HTTP — proxies, firewalls, CORS all work the usual way.
  • Browser-native — no polyfill.
  • Automatic reconnect on disconnect (the retry: directive).
  • One-way, so the client can't push back — small attack surface.

Cons:

  • One-way — no client-to-server message channel. Use WebSocket for that.
  • HTTP/1.1 caps at six connections per host. HTTP/2 multiplexing fixes this.
  • Not universal — IE never had it (but IE is dead).

4. WebSocket — bidirectional full-duplex

A separate protocol over TCP (ws://, wss://). HTTP upgrade handshake, then full-duplex framed messages.

// Server (ws library)
import {WebSocketServer} from "ws";
const wss = new WebSocketServer({port: 8080});
wss.on("connection", (ws) => {
  ws.on("message", (data) => {
    ws.send(`echo: ${data}`);
  });
});

// Client
const ws = new WebSocket("wss://example.com/socket");
ws.onmessage = (e) => console.log(e.data);
ws.send("hello");

Pros:

  • Bidirectional and instant. Essential for games, collaboration, trading.
  • Low overhead — just message frames.

Cons:

  • Separate infrastructure — many CDNs and HTTP caches don't handle it.
  • You implement reconnect, auth, and heartbeat yourself. No built-in retry like EventSource.
  • Scaling requires sticky sessions or a broker (Redis pub/sub).

Decision guide

Receiving external SaaS events

Webhook, no question. Polling burns provider rate limits and latency at once. Every modern SaaS supports webhooks.

Long-running job status

Polling at 5–10 s. Simple wins. Jobs under 30 s — polling is enough; over a minute — webhook + email/push, or SSE for progress.

Dashboards and live charts

SSE. One-way plus automatic reconnect. WebSocket is overkill. Push metrics every second.

Chats, collaboration, games

WebSocket. Bidirectional, ordered, low-latency — all required.

When the provider has no webhook

Polling steps in. Build the cron expression with Cron Expression Parser or Cron Expression Builder and fetch on a schedule.

Webhook + polling hybrid

The most reliable production pattern:

  1. Primary channel: webhook (instant).
  2. Backup cron: poll every 24 hours to catch missed webhooks (e.g. reconcile the last 24 hours of charges against your DB).
  3. Replay anything missing.

Polling compensates for the one webhook weakness (transient network failure). Stripe officially recommends this combination.

Common pitfalls

1. Doing heavy work synchronously in the webhook handler

Over 5 s and the provider treats it as a timeout. Respond fast, push to a queue.

2. Skipping signature verification

With just the endpoint URL, anyone can spoof events — including payments. Constant-time compare via HMAC Verify.

3. Polling too aggressively

One-second polling = 60 req/min × users. Rate limits or cost blow up. Migrate to SSE or WebSocket.

4. Ignoring sticky sessions with WebSocket

A load balancer that bounces each connection to a different server breaks broadcast. Use Redis pub/sub or sticky sessions.

5. Idle timeouts on SSE/WebSocket

Proxies and CDNs drop idle connections. Send heartbeats every 15–30 seconds.

6. Treating the webhook URL as a secret

Not enough. Combine signature verification with hard-to-guess URLs and provider IP allowlisting (if available).

7. Misreading webhook response codes

For webhook receivers, 200 = "I received it." Don't use 4xx/5xx for business outcomes — those trigger provider retries. See HTTP Status Codes for class meanings.

Summary

  • External SaaS events = webhook (instant, minimal cost).
  • Polling is simple but costs as much as it saves. Aim for ≥ 5 s intervals.
  • One-way live = SSE. Two-way = WebSocket.
  • Webhooks: always verify signatures, respond in < 5 s, enqueue real work.
  • Webhook + polling hybrid is the production reliability standard.
Back to guides