"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
| Pattern | Direction | Latency | Cost | Typical use |
|---|---|---|---|---|
| Polling | Client → Server (repeated pull) | interval (seconds–minutes) | requests/interval × n | Job status, fallback for missing webhooks |
| Webhook | Server → Server (event push) | near-instant | one per event | Stripe charges, GitHub push, Slack commands |
| SSE | Server → Client (stream) | instant | one connection per client | Chats, dashboards, one-way live updates |
| WebSocket | Bidirectional (full-duplex) | instant | one connection, both ways | Collaboration, 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 secondsPros:
- 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:
- Primary channel: webhook (instant).
- Backup cron: poll every 24 hours to catch missed webhooks (e.g. reconcile the last 24 hours of charges against your DB).
- 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.