Every integration that needs to react to another system's events comes down to one of two designs: keep asking "did anything change yet?" or have the other system tell you the second it does. I went deep on the actual math and security tradeoffs of both in a longer writeup on DevToolLab, but here's the short version, including the part that surprised me: how much of a polling loop is pure waste.
Polling: simple, and mostly wasted requests
Polling is calling a GET endpoint on a timer and diffing against what you saw last time. It needs nothing special from the API you're calling, no public endpoint on your end, no signature scheme to implement. That simplicity is also its whole problem: you're guessing at a check interval and paying for the guess every single time, whether or not anything happened.
I ran the numbers for a resource that changes about once an hour:
def polling_requests_per_day(interval_seconds: int) -> int:
return (24 * 60 * 60) // interval_seconds
def waste_percent(interval_seconds: int, events_per_day: int) -> float:
total = polling_requests_per_day(interval_seconds)
wasted = max(0, total - events_per_day)
return round(wasted / total * 100, 2)
for s in [5, 10, 30, 60]:
print(s, "s ->", waste_percent(s, events_per_day=24), "% wasted")
5 s -> 99.86 % wasted
10 s -> 99.72 % wasted
30 s -> 99.17 % wasted
60 s -> 98.33 % wasted
Even a lazy 60-second interval throws away 98.33% of its requests. That waste isn't free for the provider either, which is why hammering an API on a short interval runs into rate limits fast. GitHub caps authenticated REST calls at 5,000 per hour, and a service polling several repos every few seconds can burn through that before lunch.
Webhooks: push instead of pull
A webhook is the same idea flipped around. You register a URL once, and the provider POSTs to it the moment something happens; no interval to tune, no guessing. The cost moves from "requests you throw away" to "a public endpoint you now have to secure and keep up." Stripe, GitHub, and Shopify all work this way already, it's a pattern you're probably already consuming even if you haven't built one yourself.
Verifying a webhook is actually real
Any endpoint that accepts unauthenticated POST requests from the internet can be spoofed unless you check who sent it. Stripe's approach is worth learning because most providers copy its shape: a Stripe-Signature header carrying a timestamp and an HMAC-SHA256 digest computed over {timestamp}.{raw_body}, keyed with your endpoint secret.
import hashlib, hmac, time
def verify_signature(payload: str, sig_header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in sig_header.split(","))
signed_payload = f"{parts['t']}.{payload}"
computed = hmac.new(secret.encode(), signed_payload.encode(), hashlib.sha256).hexdigest()
if abs(time.time() - int(parts["t"])) > tolerance:
return False
return hmac.compare_digest(computed, parts["v1"])
Two details matter more than they look: use hmac.compare_digest, never ==, because a plain string comparison leaks timing information an attacker can use to guess the signature byte by byte. And check the timestamp - Stripe's own libraries default to a 5-minute tolerance, because without it a captured valid signature can be replayed forever.
Delivery isn't guaranteed, so build for retries
Polling heals itself: a failed request just gets tried again next tick, and you already have current state. Webhooks don't have that by default. If your endpoint is down mid-deploy when an event fires, that delivery is gone unless the provider retries.
Stripe retries with exponential backoff for up to three days before disabling the endpoint, which is generous but means two things are guaranteed to happen eventually: a delivery arrives hours late, and the same event arrives more than once. Store the event ID and skip duplicates. It's a small check to write and an easy one to skip, right up until a refund gets processed twice in production.
If you'd rather not hand-roll retry logic, signing, and delivery logs for webhooks you send out yourself, Svix is an open source, MIT-licensed, self-hostable option that does the Stripe-style reliability model for you.
Which one to actually use
Poll when there's no webhook option, when you only need fresh data at the moment someone's looking at a screen, or when you're reconciling after downtime. Reach for webhooks when near-real-time matters, when "nothing changed" checks would show up on a bill, and whenever the provider offers them at all, which by now is most payments, source control, and commerce platforms.
Most production systems end up running both: webhooks for the real-time path, plus a slow reconciliation poll every few hours as a safety net for the rare delivery that never arrives. I go into the full decision framework, plus more of the signature-verification edge cases, in the original DevToolLab article.
References
- Original article on DevToolLab
- HTTP Status Checker - confirm your webhook receiver returns a 2xx before a provider starts retrying against it
- HAR File Analyzer - capture network traffic and see how many polling requests fired versus how much real data came back
- Stripe webhook signature docs
- Svix



Top comments (0)