Polling vs Webhooks: When to Use One Over the Other
January 16, 2026
Last updated: September 20th 2026
Use webhooks when your product has to react to changes as they happen and the changes are sparse; poll when a delay of minutes to hours is acceptable, the integration offers no reliable webhook, or you can't expose an inbound endpoint.
For production data sync, the most resilient design is a hybrid: an initial API sync, webhooks as the low-latency change signal, and periodic reconciliation from the API to repair anything missed, because webhook deliveries can be delayed, duplicated, out of order or dropped, as Stripe and Microsoft both document. On Unified.to, a virtual webhook covers the integrations whose APIs have no webhooks, so the same hybrid works across every integration.
How do polling, native webhooks and virtual webhooks compare?
Polling is client-driven and works with any API but is delayed by its interval; native webhooks are event-driven but depend entirely on the integration; virtual webhooks are event-driven from your application's side while the checking runs inside Unified.to. The table sets out the dimensions a team weighs.
| Dimension | Polling | Native webhooks | Virtual webhooks (Unified.to) |
|---|---|---|---|
| Who detects the change | Your application | The source integration | Unified.to |
| Works when the integration has no webhooks | Yes | No | Yes |
| Average delay | Half your polling interval | As the integration sends it, subject to its queueing | Half your configured interval (as of September 2026, 1 minute minimum on paid plans, 60 on free) |
| Requests that return nothing | Count against the integration's rate limit and your infrastructure | None | Count against the integration's rate limit; not billed by Unified.to |
| Inbound endpoint required | No; outbound only | Yes, HTTPS with signature verification | Yes, HTTPS with signature verification |
| Code you maintain | Scheduler, checkpoints, pagination, dedup, retries, backoff | Endpoint, verification, idempotency | Endpoint, verification, idempotency |
| Historical data | Read it yourself | Not from the webhook | Yes, via include_all initial sync |
| Retry when delivery to you fails | Not applicable | Varies by integration; on Unified.to, 3 immediate retries then marked unhealthy | 3 immediate retries, then progressive backoff for up to roughly two weeks |
| Deleted records | If the API exposes them; a current-state list may not | Where the integration sends them | Where the integration supports it |
| Completeness | You control it, with a stable cursor | Limited to the events the integration emits | Limited to what the check can detect |
| Most production systems use more than one of these. The rest of this page explains each, then the hybrid that combines them. |
For how unified API platforms implement virtual webhooks compared with sync-based notification systems, see Which Unified API Platforms Support Virtual Webhooks vs Sync-Based Notifications.
What is polling?
Polling is a pull model in which your application calls a third-party API on a timer to ask whether anything has changed; your application controls the timing, the retries and the scope of each read. A typical loop calls a list endpoint with a since-filter, processes what comes back, stores a checkpoint, waits, and repeats.
GET /crm/contacts?updated_since=2026-09-20T14:00:00Z&limit=100
# Returns records changed since the checkpoint; store the newest updated_at as the next checkpoint
Where the API supports conditional requests, an ETag with If-None-Match lets the server answer 304 Not Modified with no body when nothing changed, as defined in RFC 7232. The request still counts against the rate limit; it just costs less bandwidth.
GET /crm/contacts/123
If-None-Match: "abc123"
# 304 Not Modified if unchanged; 200 with the record if changed
When does polling win?
Polling wins when the acceptable staleness is measured in minutes or hours, when almost every check returns something, when the integration has weak or no webhooks and you aren't using an integration layer, or when you can't expose an inbound endpoint. Two further cases are worth naming because they cut against the usual advice.
First, polling gives you a snapshot of current state each time, which makes reconciliation and deletion detection easier than an event stream. A deleted record simply stops appearing, whereas a webhook has to emit an explicit delete event, and not every integration does; on Unified.to, virtual webhooks detect deletions only where the integration supports it.
Second, for a small integration, a cron job that polls once a minute is a few lines of code, while an HTTPS endpoint with signature verification, durable ingestion and idempotent processing is more work. Polling is also the natural fit for checking whether an export or report job has finished when no completion event exists.
Where does polling break down?
Polling breaks down at scale because most requests return nothing while still consuming the integration's rate limit and your infrastructure, and because the delay is built in: on average a change waits half the interval before you see it, and at worst the full interval. Shrinking the interval reduces the delay but multiplies requests linearly.
A useful capacity estimate is requests per minute equals active connections, times polls per connection per minute, times pages per poll. A one-minute interval across 1,000 customer connections is 1,000 requests a minute, or 1.44 million a day, before pagination, and most of them find nothing. GitHub's own webhook documentation makes the same point: polling many resources consumes API quota quickly, while webhooks deliver data only when an event occurs.
Naive timestamp polling also misses records through clock skew, timestamp precision and equal timestamps, which is why robust polling uses overlap windows and deduplicates by record ID. AWS's Well-Architected guidance on limiting retries adds exponential backoff, jitter and a retry cap, so that a tight loop doesn't trigger the rate limits it's trying to stay under.
What is a webhook?
A webhook is a push model in which your application registers an HTTPS endpoint and the source integration sends an HTTP request to it when a subscribed event occurs, with information about the change in the payload. GitHub describes it as receiving data when an event happens rather than intermittently calling an API to check. Your application subscribes once and then receives events.
When do native webhooks win?
Native webhooks win when users expect an immediate reaction (a payment confirmed, an application received, a build finished), when changes are sparse across a large number of resources, and when the integration offers a mature event system: signed requests, event IDs, retries, delivery logs and documented ordering semantics. They remove the empty requests that polling makes, and a single event can fan out to several consumers without separate polling logic per consumer.
What are the limits of native webhooks?
Native webhooks depend entirely on the source integration, and that dependency is the limit: support, retry behaviour, ordering, subscription lifetime and payload shape all vary by integration, and many APIs offer no webhooks at all or only for some objects. Even mature senders make no strong guarantees. As of September 2026, Stripe's webhook documentation states that it doesn't guarantee events are delivered in the order they're generated, that endpoints may receive the same event more than once, and that consumers should track event IDs and retrieve missing objects through the API. Microsoft Graph retries a failed delivery for up to four hours and documents that dropped notifications can't be recovered; consumers who receive a missed lifecycle event are directed to resync through a delta query.
Retry windows are finite. Stripe retries for up to three days in live mode; Microsoft Graph for four hours. An outage longer than the window creates a permanent gap unless something reconciles it.
On Unified.to, a native webhook delivery that your endpoint doesn't acknowledge is retried three times immediately, about a second apart; if those fail, the webhook is marked unhealthy and the event isn't redelivered unless the integration sends it again. Because the integration already delivered successfully to Unified.to, its own retry window doesn't apply. If your endpoint can be down for longer than a few seconds, use a virtual webhook where one is available, or reconcile from the API as described below.
Two things Unified.to does change what a consumer has to handle. Where an integration's event contains only an identifier, Unified.to fetches the full record before delivery, so the payload carries the data rather than a pointer. Microsoft Graph, by contrast, distinguishes basic notifications that carry an identifier from rich notifications that include resource data. And every delivery carries the same payload format and signature regardless of which integration produced it.
What is a virtual webhook?
A virtual webhook is a webhook Unified.to provides for an integration whose API doesn't offer one: Unified.to checks the integration for changes on an interval you set, detects what changed since the last successful delivery, and delivers those changes as events to your webhook URL. From your application's side it behaves like any other webhook. You subscribe once, events arrive, and there's no polling logic in your system.
As of September 2026:
- Unified.to reads the integration on your interval, as often as every minute on paid plans and every 60 minutes on the free plan.
- Changes are detected using timestamps and cursors. The read position only advances after a successful delivery to your endpoint.
- Nothing is stored between checks. If a delivery fails, the retry re-reads the same page from the integration rather than resending a stored payload, with progressive backoff for up to roughly two weeks.
- Deleted records are detected where the integration supports it; check the object's Feature Support tab.
The scheduled checks happen inside Unified.to rather than in your application. This is distinct from sync-based systems, where the platform stores your customers' data and emits events after scheduled updates.
What is the recommended architecture for production sync?
Treat a webhook as a prompt to synchronize, not as the sole source of truth: run an initial sync, use webhooks as the change signal, apply changes idempotently, and reconcile from the API on a schedule so that a missed event doesn't silently corrupt your copy. Stripe recommends retrieving missing objects through its API when webhook ordering creates gaps, and Microsoft directs clients to resync after a missed notification; both are linked above. On Unified.to the pattern maps to specific features.
- Initial sync. Create the webhook with
include_all=true. Unified.to reads the connection's existing records page by page and POSTs each page taggedINITIAL-PARTIAL, ending withINITIAL-COMPLETE. Native and virtual webhooks share this path, and the interval has no effect on it. - Webhook ingest. Verify
sig256, validate theexternal_xrefagainst the account you expect, write the delivery to a durable queue, and return a success status quickly. Do heavy processing afterwards. - Idempotent processing. Update a record only when the incoming
updated_atis newer than what you hold, atomically, and ignore anything older. This handles duplicates, rapid successive updates and retries in one rule. - Reconciliation. On a schedule that matches how much staleness you can tolerate, list the object with
updated_gteset to your last reconciliation time and repair anything the webhook missed. For integrations where virtual webhooks can't detect deletions, this is also where you find them, by comparing IDs. - Gap recovery. When a webhook goes unhealthy, fix the endpoint or the connection's auth and recreate it. Recreating with
include_all=trueruns the initial sync again, which re-delivers everything and is the blunt way to close a gap. - Monitoring. Alert on
is_healthyturning false (via the notifications webhook, below), on queue lag, on duplicate rate, and on reconciliation drift.
Which should you choose for a given situation?
Choose by the situation, not the technology; the table gives a default for the common ones.
| Situation | Better default | Why |
|---|---|---|
| Payment succeeded or failed | Webhook, then verify through the API | Latency matters; duplicates and ordering must be handled |
| Nightly analytics import | Polling or bulk export | Periodic completeness matters more than speed |
| CRM or ATS synchronization | Hybrid | Webhooks give speed; reconciliation gives completeness |
| Integration whose API has no webhooks | Virtual webhook | Same subscription model, no polling code in your system |
| Thousands of mostly inactive connections | Webhook, virtual where native isn't offered | Polling would be mostly empty requests |
| Async report or export generation | Polling, unless a completion event exists | Finite state transitions; client-controlled backoff is simple |
| System behind a strict firewall with no inbound endpoint | Polling | Outbound-only is operationally simpler |
| Exact local replica of an integration's data | Hybrid with scheduled reconciliation | Only a reconciliation pass establishes completeness |
| User clicks "Refresh now" | On-demand read, or a manual trigger on a virtual webhook | The user is asking for current state now |
| Integration whose webhooks lack signatures, retries or IDs | Polling or virtual webhook | A weak push channel isn't worth the gaps |
| A practical decision sequence: does the integration support the event and fields you need natively, and if not, is a virtual webhook available? What staleness is acceptable? How sparse are changes? Can you run an inbound endpoint? What are the recovery semantics? And what, if anything, establishes completeness? If the answer to the last one is "nothing," add reconciliation regardless of the mechanism. |
How do latency and cost compare in numbers?
Polling latency averages half the interval and peaks at the full interval, and the same arithmetic applies to a virtual webhook's interval; native delivery is bounded by the integration's own queueing rather than by anything you configure. The figures below are derived from the interval, not measured.
| Polling every 5 minutes | Polling every 60 seconds | Virtual webhook, 60-minute interval (free) | Virtual webhook, 1-minute interval (paid) | Native webhook | |
|---|---|---|---|---|---|
| Average delay | 2.5 minutes | 30 seconds | 30 minutes | 30 seconds | Integration-dependent |
| Worst case | 5 minutes | 60 seconds | 60 minutes | 60 seconds | Integration-dependent |
| Requests to the integration per connection per day | 288 | 1,440 | 24 | 1,440 | 0 |
| Cost is where the models diverge. With polling, every one of those requests is yours to make and pay for, whether or not it finds anything. With a virtual webhook, the checks still run against the integration and count against its rate limits, but as of September 2026 Unified.to bills only per page of changes successfully delivered to your endpoint; checks that find nothing, failed delivery attempts and retriable read errors aren't billed. For illustration, 1,000 connections on a one-minute interval is 1.44 million checks a day; if 2% of checks find changes and each fits in one page, that's 28,800 billed deliveries, not 1.44 million. |
How do you create and receive a Unified.to webhook?
You create a webhook by POSTing to /unified/webhook with a connection, an object type, an event and your endpoint URL, and you receive deliveries as signed POSTs in the same format the corresponding API endpoint returns. The full field reference is on the Create webhook subscription page.
POST /unified/webhook?include_all=true
{
"connection_id": "CONNECTION_ID",
"hook_url": "https://example.com/webhooks/unified",
"object_type": "crm_contact",
"event": "updated",
"webhook_type": "virtual",
"interval": 5,
"fields": "id,name,emails",
"filters": { "company_id": "12345" }
}
webhook_type is optional; leave it out and Unified.to picks a type the object supports, and requesting an unsupported type returns 400. It can't be changed after creation. interval applies to virtual webhooks only. filters are supported on virtual webhooks and on native webhooks for many integrations; see How to filter webhook events.
Verify each delivery before trusting it. The signature is an HMAC-SHA256 over the serialized data array followed by the nonce, keyed with your workspace secret, compared in constant time:
import { createHmac, timingSafeEqual } from 'crypto';
function verifyUnifiedWebhook(workspaceSecret: string, body: {
data: unknown[]; nonce: string; sig256: string;
}): boolean {
const computed = Buffer.from(
createHmac('sha256', workspaceSecret)
.update(JSON.stringify(body.data))
.update(body.nonce)
.digest('base64'), 'utf8');
const provided = Buffer.from(body.sig256, 'utf8');
return computed.length === provided.length && timingSafeEqual(computed, provided);
}
Then apply the change idempotently:
-- Only apply if the incoming record is newer than what we hold
UPDATE contacts
SET name = $2, emails = $3, updated_at = $4
WHERE id = $1 AND updated_at < $4;
If more records changed than fit in one page, you receive several POSTs. The type field on each delivery tells you whether it's part of the initial sync (INITIAL-PARTIAL, INITIAL-COMPLETE) or an ongoing event (VIRTUAL, NATIVE).
How do you know when a webhook has stopped?
On Unified.to, every webhook has an is_healthy field, and once it's false the webhook stops reading and delivering and does not resume on its own; you fix the endpoint or the connection's auth, then recreate the webhook. There's no re-enable. To be told when it happens, set a notifications webhook URL in the dashboard under Settings > Workspace and subscribe to the WEBHOOK_UNHEALTHY event, at the time you create your first webhook rather than after one breaks. The dashboard's audit trail shows recent runs and records delivered, and How to troubleshoot unhealthy webhooks covers diagnosis.
Monitoring health is your responsibility on any webhook model. Stripe and Microsoft both put the burden of detecting gaps on the consumer; Unified.to gives you the signal, but you have to watch it. A polling loop has no equivalent signal at all; you find out it's stopped when the data goes stale.
Frequently asked questions
Can webhooks deliver historical data?
On Unified.to, yes. Create the webhook with include_all=true and the connection's existing records are delivered in pages before ongoing events begin. Generic webhooks, including Stripe's and GitHub's, deliver new events only, so a separate backfill is normally required.
Do virtual webhook checks count against the integration's API rate limits? Yes. Each check calls the integration's API and counts against its rate limits; Unified.to backs off automatically and honours the integration's retry-after guidance. What virtual webhooks remove is the request volume from your own infrastructure and any charge for checks that find nothing.
How often should a virtual webhook check? As often as the data needs and no more. The minimum is 1 minute on paid plans and 60 on free. A one-minute interval on a rarely changing object costs almost nothing, because empty checks aren't billed, but it still counts against the integration's rate limit, so match the interval to how quickly your product has to react.
Can a virtual webhook detect deleted records? On some integrations. It depends on whether the integration supports deletion detection through the check Unified.to runs; confirm it on the object's Feature Support tab. Where it doesn't, find deletions during reconciliation by comparing IDs.
Can I switch a webhook from virtual to native later? Not in place. The type is set at creation and ignored on update; delete the webhook and create a new one. Native and virtual use different mechanisms, so switching in place would risk duplicates or broken subscriptions.