Webhooks vs polling: when each one wins

A practical guide to integration tradeoffs. Latency, reliability, debugging, and which one your tool actually supports.

6 min read·Updated Apr 28, 2026

"Webhooks are real-time, polling is wasteful, always use webhooks." That's the conventional wisdom and it's wrong about 30% of the time. Polling is the right call more often than people admit, especially in 2026 where serverless cold starts make webhook handling weirder than it should be.

Before you can pick, you have to actually know what each one does, in plain English.

Webhook: Tool A pushes to your URL when an event happens. You react. Push-based.

Polling: You ask Tool A "what's new since last time" on a schedule. Pull-based.

That's the whole vocabulary. Everything else is tradeoffs.

The four differences that actually matter

1. Latency. Webhooks land in roughly 100ms to 1 second. Polling lands as fast as your interval, which is usually 1 to 15 minutes. If a human is staring at a screen waiting for something to update, you need webhooks. If a cron job at 2am is reconciling yesterday's data, polling is fine. Most "real-time" requirements aren't.

2. Reliability. Webhooks have a retry window, and the window is shorter than you think. Stripe retries failed deliveries for 3 days. GitHub gives up after 8 hours. Slack tops out at 5 minutes. If your endpoint is down past that window, the event is gone forever. Polling is structurally different. You're going to ask in 5 minutes anyway, so a missed cycle just means you catch up next cycle. Polling is self-healing. Webhooks are not.

3. Idempotency. Webhooks can deliver the same event twice. Sometimes more. Network blip, the receiver took 6 seconds instead of 5, the sender retried, now you've processed the same payment twice. Your handler MUST be idempotent, every time, no exceptions. Polling typically isn't double-delivery prone, as long as your "last seen" cursor is honest. The difference matters when the event in question is "charge a credit card."

4. Debugging. When a webhook fails, you don't see it. The request never hit your server. You have to log into the sender's dashboard, find the delivery attempt, read their error message. With polling, every request you made is in your own logs, with your own request ID, in the format you already grep. The visibility gap is bigger than people realize until the first 2am incident.

When webhooks win

Real-time UX. A user just paid, you need to flip them to "Pro" before they refresh. Stripe payment events. Slack and Discord bots, where a 5-minute lag makes the bot feel broken. Anything where the delay between event and reaction directly damages conversion or trust.

When polling wins

Daily or hourly background sync, where the job runs at 2am and goes back to sleep. APIs that don't offer webhooks, which is more of them than vendors admit. Low-volume integrations where the setup overhead of webhook signature verification, retry handling, and queue plumbing exceeds the 5 minutes a day you'd spend polling. Hobby apps and small projects where your handler can't be guaranteed online. And anytime you genuinely want to batch, because polling naturally batches and webhooks naturally don't.

A 15-line polling cron

This is a Vercel cron that polls every 5 minutes. The pattern is the same in any runtime.

// app/api/cron/sync/route.ts
export async function GET() {
  const lastSeen = await kv.get<number>("sync:last_id") ?? 0;
  const res = await fetch(`https://api.example.com/events?since_id=${lastSeen}`, {
    headers: { Authorization: `Bearer ${process.env.API_KEY}` },
  });
  const events: { id: number; payload: any }[] = await res.json();
  for (const ev of events) await processEvent(ev);
  if (events.length) await kv.set("sync:last_id", events.at(-1)!.id);
  return Response.json({ processed: events.length });
}

The whole pattern is the cursor. Store the last ID you saw, ask for everything after it, update the cursor when you're done. No queues, no signature verification, no retry windows. If a run fails, the next run picks up from the same cursor.

The webhook reliability gotcha

The single most common webhook bug is doing the work inline. The sender opens a connection, you spend 8 seconds processing the event, the sender times out at 5 seconds, marks it failed, retries. Now you're processing the same event in parallel with itself.

The fix is the "200 immediately, process async" pattern. Your handler does two things: validate the signature, push the payload onto a queue. Then return 200 in under a second. A separate worker processes the queue. Your endpoint stays fast, the sender stays happy, and retries stay rare.

export async function POST(req: Request) {
  const body = await req.text();
  if (!verifySig(body, req.headers.get("x-signature"))) {
    return new Response("bad sig", { status: 400 });
  }
  await queue.enqueue("webhook-events", body);
  return new Response("ok", { status: 200 });
}

The hybrid that real production runs

Mature systems usually run both. Webhooks for the fast path so the UI updates immediately, plus a daily polling reconcile job that pulls everything from the last 24 hours and fills any gaps the webhooks dropped. Belt-and-suspenders. The webhooks give you latency, the polling gives you a guarantee. When the webhook stream silently breaks at 3am, the morning reconcile catches it, and you find out from your own logs instead of an angry support email.

Next pillar, "Zapier vs Make vs n8n vs scripts." Once you've decided how data gets to you, the next question is what runs the glue.