Webhooks = reversed HTTP
Instead of you polling a provider, the provider POSTs an event to your endpoint when something happens (payment succeeded, PR opened). Three properties define correct webhook handling:
- Delivery is at-least-once, never exactly-once. A network can fail after the provider sent but before it got your
200, so it retries — you will receive duplicates. Handlers must be idempotent: dedupe on the event's unique ID (store processed IDs). - Return
200fast, process async. Providers enforce short timeouts (GitHub ~10s). Do the minimum — verify signature, enqueue, return200— then process on a worker. Slow handlers cause timeouts → the provider marks it failed and retries, amplifying load. This is the single most important webhook rule. - Verify the signature (HMAC). The provider signs the raw body with a shared secret; you recompute the HMAC and constant-time-compare. Verify against the raw bytes before any JSON parsing/middleware mutates them — a classic bug — and use a constant-time compare (
crypto.timingSafeEqual) to avoid timing attacks.
Canonical specifics: Stripe's Stripe-Signature is t=<ts>,v1=<HMAC-SHA256 of "{t}.{raw_body}">; reject if the timestamp is outside the 5-minute tolerance (replay protection); each event has an evt_... id for dedupe. GitHub uses X-Hub-Signature-256 plus X-GitHub-Delivery (a unique UUID).
Streaming
- SSE (Server-Sent Events): one long-lived HTTP response, server→client only,
Content-Type: text/event-stream, auto-reconnect viaLast-Event-ID. HTTP/proxy-friendly and the mechanism under LLM token streaming (OpenAIstream: true→deltachunks ending in[DONE]; Anthropic emits typedcontent_block_deltaevents). Streaming slashes time-to-first-token — the UX reason every chat app streams.