Two systems need to stay in sync — do you push events or pull for changes? A practical guide to the trade-offs, and why the honest answer is often a bit of both.
- Webhooks are immediate and efficient but fragile — a single delivery, gone if you miss it. Polling is slower and wasteful but self-healing.
- A webhook endpoint has non-negotiable homework — verify signatures, respond in milliseconds, dedupe, and process asynchronously.
- For data that matters, run webhooks for speed and a periodic reconciliation poll as a safety net. That combination catches the silent data loss that webhooks alone will eventually cause.
Whenever two systems need to stay in step, someone has to answer a deceptively simple question: how does one system find out that something changed in the other? There are only two answers. Either the system where the change happened pushes a message to say so — a webhook — or the system that cares keeps asking "anything new?" on a schedule — polling.
This sounds like a low-level detail, and it gets decided offhandedly all the time. But it quietly sets how timely, how reliable, and how expensive the integration will be. Choose badly and you get data that is always slightly stale, or a system that hammers an API for nothing, or an integration that silently drops updates until the numbers stop adding up. The trade-offs are worth understanding properly — and, quite often, the right answer is to use both.
Two directions, and everything follows from that
Polling is the straightforward one. On a schedule — every minute, every five, every hour — your system asks the other for anything that has changed since it last looked, and processes what comes back. It is you, repeatedly checking the letterbox.
A webhook flips the direction. The moment something happens, the other system sends a message to an address you gave it, and you react. It is the postman knocking when there is post, rather than you checking the box. That directional difference is the whole story — timeliness, cost, and failure modes all follow from it.
POLLING WEBHOOKS
┌────────┐ "anything new?" ┌────────┐ event happens
│ you │ ───────────────▶ API │ API │ ──────────┐
│ │ ◀─────────────── API │ │ ▼
└────────┘ "no" (x999) / "yes" └────────┘ ┌────────┐
you initiate, on your schedule they initiate │ you │
the instant └────────┘
it mattersThe webhook trade-off
Webhooks have two real advantages. They are immediate — no lag between the change and your knowing about it, which matters enormously for anything time-sensitive: a payment that should trigger fulfilment at once, a support ticket that needs a fast response. And they are efficient — you do work only when there is work to do, with no thousand wasted "anything new?" requests that all hear "no."
If webhooks were purely better, polling would not exist. The catch is that they are harder to make reliable, and the difficulty hides entirely in the failure paths. A webhook is a message sent once; miss it and it is gone. Your endpoint was down for two minutes during a deploy? Those events may never come again. Your handler threw halfway through? You may have lost that update with no record it existed. That silent loss is the worst kind, because you find it only when the totals disagree.
So a webhook endpoint has homework that the cheerful demo skips. It must be publicly reachable, always up, and fast — providers give you a short budget, often a couple of seconds, before they call the delivery failed. You must verify signatures, because you have opened a door to the public internet. You must dedupe, because providers legitimately resend. And you must cope with events arriving out of order.
The single most important structural rule: verify, acknowledge, then process — in that order, and process asynchronously. Do the real work inside the request and you will blow the timeout, the provider will retry, and now you are processing the same event twice while it thinks you failed.
import crypto from "node:crypto";
export async function POST(req: Request) {
const raw = await req.text(); // verify against the raw body, never the parsed JSON
const signature = req.headers.get("x-signature") ?? "";
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET!)
.update(raw)
.digest("hex");
// Constant-time compare so an attacker can't probe the secret via timing.
const ok = crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return new Response("bad signature", { status: 401 });
const event = JSON.parse(raw);
await enqueue(event); // hand off to a queue; do NOT process inline
return new Response("ok", { status: 200 }); // ack fast, well inside the timeout
}Deduplication is the other half. Providers resend, so the same event ID can arrive more than once. Record processed IDs and drop repeats — an idempotent handler turns "they sent it twice" from a duplicate order into a no-op.
async function handleEvent(event: { id: string; type: string; data: unknown }) {
// INSERT ... ON CONFLICT DO NOTHING — atomic claim of this event id.
const claimed = await db.markProcessed(event.id);
if (!claimed) return; // already handled this one; a legitimate resend
await applyChange(event);
}The polling trade-off
Polling's reputation is unfairly poor. It gets dismissed as crude, but it has a virtue that beats elegance: it is robust and self-healing. If your system is down for ten minutes, polling simply resumes and picks up everything that changed while you were away. Nothing is lost, because you are pulling current state, not depending on a one-time message you had to be present to catch. That forgiveness of ordinary failures — deploys, restarts, brief outages — is worth a great deal.
Polling is also simpler to reason about: no public endpoint to secure and keep alive, no delivery guarantees to chase. You control the pace entirely. The trick that makes it efficient is a cursor — remember where you got to, and ask only for what is newer, so each poll is cheap even when the dataset is huge.
async function poll() {
const since = await getCursor(); // e.g. last updated_at we saw
const changes = await api.listChanges({ updatedAfter: since, limit: 100 });
for (const change of changes) {
await applyChange(change); // idempotent — a re-poll must not double-apply
}
if (changes.length > 0) {
await saveCursor(changes[changes.length - 1].updatedAt);
}
}The weaknesses are the mirror of webhooks' strengths. Polling is not immediate — data is stale by up to one interval, so a one-minute cycle means a change can be a minute old before you act. Shorten the interval for freshness and you pay in wasted requests, because most cycles find nothing. If changes are rare, you might make hundreds of empty calls for every useful one, burning rate limit to hear "nothing new."
Side by side
| Webhooks | Polling | |
|---|---|---|
| Latency | Near-instant | Up to one interval |
| Efficiency | Work only on real events | Many empty requests |
| Recovery from downtime | Poor — misses are lost | Excellent — self-healing |
| Build complexity | High — public endpoint, signatures, dedup, ordering | Low — a scheduled job with a cursor |
| Rare events | Ideal | Wasteful |
| Provider support | Not always offered or reliable | Always possible if there's a list API |
| Worst failure mode | Silent data loss | Staleness / rate-limit exhaustion |
Why the answer is often both
Here is what experience teaches: these are not rivals. Their strengths and weaknesses are complementary, and the most reliable integrations use them together. Run webhooks for timeliness — the fast reaction when everything works, which is most of the time — and a periodic reconciliation poll, less often, that sweeps up anything the webhooks missed and confirms the two systems genuinely agree.
real time ┌──────────┐ apply
event ────▶ │ webhook │ ──────▶ your system
└──────────┘ ▲
│ fills gaps, corrects drift
every 6h ┌──────────┐ compare │
sweep ────▶ │ poll │ ────────────┘
└──────────┘The reconciliation poll is what quietly catches the silent data loss that would otherwise surface as a mystery discrepancy weeks later. It costs a little more to build, but it removes the single scariest failure mode of webhooks — losing data without knowing — and that is usually worth it for anything important.
How to choose
Stripped down, the decision is a few honest questions.
- 01How quickly must you react? Seconds → webhooks. Minutes or hours → polling may be all you need.
- 02How often do changes happen? Frequent → polling is reasonable. Rare → constant polling is mostly noise; lean webhooks.
- 03How costly is a missed or delayed update? The higher the cost of getting it wrong, the more a reconciliation safety net earns its place.
- 04Does the provider even offer webhooks, and are they reliable? Not all do, and not all that do are dependable. Sometimes polling is simply the only solid option.
Answer those honestly and the shape usually reveals itself. And do not dismiss "both" as over-engineering. For any integration where the data really matters, an immediate webhook plus a periodic reconciliation is not belt-and-braces excess — it is just what reliable looks like.