Webhooks & Realtime
Webhooks, polling, SSE, WebSockets. Signed payloads.
Most APIs are pull: the client asks, the server answers. But sometimes the server has news and needs to tell the client. Sometimes the news has to arrive in milliseconds. This lesson covers the four main ways servers push: webhooks, polling, Server-Sent Events, and WebSockets. Pick the wrong one and you either waste resources or build something flaky. Pick the right one and your app feels alive.
Webhooks: the reverse API
You give Stripe (or Shopify, or GitHub) a URL. When something happens on their side, they POST to your URL. You handle it. You are now their client, in reverse. That's a webhook.
POST /webhooks/stripe HTTP/1.1
Host: api.example.com
Content-Type: application/json
Stripe-Signature: t=1716590000,v1=abcdef0123...
{
"id": "evt_123",
"type": "checkout.session.completed",
"data": { "object": { "id": "cs_456", "amount_total": 9900 } }
}Three things every webhook handler must do:
- Verify the signature. Without this, anyone who knows your URL can POST fake events. Stripe and GitHub sign payloads with HMAC-SHA256 using a shared secret.
- Return 2xx quickly.Most providers retry on 4xx or 5xx. Do the heavy work async - push to a queue, return 200, process later. If you take 30 seconds in the handler, the provider may time out and retry, and now you've got a duplicate.
- Be idempotent. Providers may send the same event twice. Dedupe on the event id.
HMAC signing in practice
The provider takes the raw request body, prepends a timestamp, hashes with HMAC-SHA256 using your shared secret, and sends the result in a header. You recompute it and compare.
import crypto from "node:crypto";
function verifyWebhook(rawBody, header, secret) {
// header: "t=1716590000,v1=abcdef..."
const parts = Object.fromEntries(
header.split(",").map((s) => s.split("=")),
);
const timestamp = parts.t;
const sent = parts.v1;
// Replay protection: reject events older than 5 minutes
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
throw new Error("replay attack - timestamp too old");
}
const signedPayload = timestamp + "." + rawBody;
const expected = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
// Timing-safe compare - never use === for crypto strings
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sent),
);
if (!ok) throw new Error("invalid signature");
}JSON.parseand re-stringify, key order and whitespace can change. The HMAC won't match. Configure your framework to give you the raw bytes for the webhook route.Polling: the dumb-but-it-works baseline
Polling means "ask repeatedly." The client hits an endpoint every N seconds asking "anything new?" Most of those requests return nothing. It wastes bandwidth and battery. But it's trivial to implement, works through every firewall, and is fine if updates are rare.
setInterval(async () => {
const res = await fetch("/api/messages?since=" + lastSeen);
const messages = await res.json();
if (messages.length) render(messages);
}, 5000);Long polling: a less-dumb baseline
Instead of returning immediately when there's nothing new, the server holds the request open for up to 30 seconds, then either sends data or times out. Client immediately reconnects. Cheap, works through proxies, but ties up server connections.
Server-Sent Events: one-way real-time over plain HTTP
SSEis a long-lived HTTP response that the server keeps writing to. Each chunk is an "event." The browser has a built-in EventSource client. Reconnection, message IDs, and resume-on-reconnect are handled for you.
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-store
Connection: keep-alive
event: message
data: {"text":"first message"}
event: message
data: {"text":"second"}
event: message
id: 17
data: {"text":"third"}
retry: 5000const events = new EventSource("/api/feed");
events.addEventListener("message", (e) => {
const msg = JSON.parse(e.data);
console.log("got:", msg.text);
});
events.addEventListener("error", (e) => {
// EventSource auto-reconnects with the last event id
console.warn("disconnected, reconnecting...");
});SSE is the right choice for: AI streaming responses, notifications, live dashboards, anything where the data flows one direction (server → client). Bonus: it works over HTTP/2 and HTTP/3, so multiple streams share one connection.
WebSockets: two-way real-time
WebSocket is a separate protocol. The connection starts as an HTTP request, then upgrades to a long-lived, full-duplex byte stream. Both sides can send anything at any time. Good for: chat, collaborative editing, multiplayer games, anything where the client also pushes frequently.
const ws = new WebSocket("wss://api.example.com/chat");
ws.addEventListener("open", () => ws.send(JSON.stringify({ join: "room1" })));
ws.addEventListener("message", (e) => {
const msg = JSON.parse(e.data);
render(msg);
});
ws.addEventListener("close", () => {
// No auto-reconnect - you implement it
});WebSockets are powerful but more work: you write framing logic, reconnection, backoff, ping/pong heartbeats, and authentication (often a token in the URL or first message because Authorizationheaders don't work in browser WebSockets).
Decision matrix
- Updates rare, latency forgiving (every few minutes is fine) → polling. Don't overthink.
- Cross-system events from a third party (Stripe, GitHub) → webhooks. Sign and dedupe.
- Server pushes, client mostly listens (notifications, AI streaming, dashboards) → SSE. Simpler than WebSockets, browser-native reconnect.
- Both sides push frequently (chat, collaboration, gaming) → WebSockets. Worth the complexity.
- Sub-50ms latency, lossy OK (voice, video, gaming) → WebRTC (uses UDP, beyond this lesson).
Quiz
A webhook handler validates the HMAC signature successfully but processes a duplicate event. What was missed?
Recap
- Webhooks: third party POSTs to your URL. Verify HMAC with the raw body, protect against replay, dedupe by event id, return 2xx fast.
- Polling: simple, wasteful. Long polling: less wasteful, more complex.
- SSE: one-way streaming over HTTP. Auto-reconnect built in. Perfect for notifications and AI streams.
- WebSockets: two-way, real-time. Worth the complexity for chat and collaboration.
- Match the protocol to the data flow shape, not to what's trendy.