Webhooks vs. Polling: How to Choose for an API Integration
By the Finvercel Engineering teamPublished 7 min read
What webhooks and polling mean
Polling means your system asks another system for changes on a schedule — for example, calling GET /orders?updated_since=… every five minutes. A webhook reverses the direction: the other system sends an HTTP request to a URL you control when something happens, such as an order being paid.
Side-by-side comparison
| Factor | Webhooks | Polling |
|---|---|---|
| Freshness | Near real time when delivery succeeds | Limited by the polling interval |
| Load and cost | Requests only when events occur | Requests even when nothing changed |
| Infrastructure | Needs a public HTTPS endpoint | Outbound requests only |
| Failure mode | Missed or duplicated deliveries | Rate limits, gaps from cursor bugs |
| Availability | Only if the provider supports them | Works with any API that lists records |
How to choose
- Check what the provider supports. If there are no webhooks for the events you need, polling is the only option.
- Define the freshness you actually need. A nightly finance report does not need second-level latency; a payment confirmation shown to a customer might.
- Plan for failure either way. Webhooks can be missed; polls can hit rate limits. Many teams use webhooks for speed and a slower reconciliation poll as a backstop.
Make webhook handlers idempotent
Providers retry deliveries when your endpoint times out or returns an error, so the same event can arrive twice. Record each event ID and skip events you have already processed:
const processed = new Set() // use a database table with a unique constraint in production
export function handleEvent(event, apply) {
if (processed.has(event.id)) return 'duplicate-ignored'
apply(event)
processed.add(event.id)
return 'processed'
}In production, store processed IDs in a database table with a unique constraint and perform the insert in the same transaction as the business update, so a crash between the two cannot cause a double-apply.
Poll with a cursor, not a full scan
Track the newest updatedAt value you have seen and request only records changed since then, following pagination:
export async function pollChanges(fetchPage, cursor) {
const changes = []
let page = await fetchPage({ updatedSince: cursor })
changes.push(...page.items)
while (page.nextPageToken) {
page = await fetchPage({ updatedSince: cursor, pageToken: page.nextPageToken })
changes.push(...page.items)
}
const newCursor = changes.reduce((max, c) => (c.updatedAt > max ? c.updatedAt : max), cursor)
return { changes, cursor: newCursor }
}A subtle bug we found while testing this sample: filtering with a strict “greater than” on the cursor skips records that share the cursor’s exact timestamp but were written after the previous poll. Prefer “greater than or equal” and deduplicate by record ID, or use an opaque cursor if the API provides one.
Common problems
- Webhook times out: acknowledge with a 2xx quickly and move slow work to a background queue.
- Events arrive out of order: compare timestamps or version numbers before overwriting newer data with older data.
- Unauthenticated requests: always verify signatures — see how to verify webhook signatures.
Related questions
Are webhooks always faster than polling?
Usually, because the source sends the event as soon as it happens. Polling is only as fresh as its interval. But webhook delivery can be delayed or retried by the provider, so do not assume instant delivery.
Can webhooks be delivered more than once?
Yes. Most providers retry on timeouts or errors, so the same event can arrive more than once. Store processed event IDs and ignore repeats.
How often should I poll?
As infrequently as your freshness requirement allows, within the provider’s rate limits. Use an "updated since" cursor so each poll only fetches changes.