Polling vs Webhooks: When to Use One Over the Other
·
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 run 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, virtual webhooks cover many integrations whose APIs have no webhooks (on supported integrations and objects), so the same hybrid works wherever Unified.to offers native or virtual webhooks.
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 | Checks at an interval you set (as of September 2026, down to 1 minute on paid plans, 60 minutes on the trial); a change arrives in half your interval on average |
| 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 |
| Across many integrations | Separate checkpoint and filter logic per integration | Support, payload shape, retries and ordering vary by integration | One subscription model, payload format and signature; what can be detected still varies by integration |
| 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 | Up to 3 immediate retries 1 second apart, then progressive backoff for up to about two weeks; marked unhealthy when retries are exhausted |
| Deleted records | If the API returns 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 request-driven 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 9110. 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 run an inbound endpoint. Two further cases 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.
- Interval trade-off. Shrinking the interval reduces the delay but multiplies requests linearly.
- Capacity. 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.
- Quota. 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.
- Shared rate limits. Where an API applies its rate limit per app rather than per customer, polling across many customers draws on one shared quota, and busy customers can starve quiet ones.
- Per-integration work. Each integration needs its own checkpoint state for every connection, and since-filters, cursors and pagination differ from one API to the next, so the polling logic is written and maintained per integration rather than once.
- Missed records. Naive timestamp polling misses records through clock skew, timestamp precision and equal timestamps, which is why reliable polling uses overlap windows and deduplicates by record ID.
- Retry storms. 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.
If the API has no webhooks at all, see how to receive updates from APIs without webhooks.
What is a webhook?
A webhook is an event-driven 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. For how webhooks and API calls fit together in one integration, see Webhook vs API: when to use each.
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. They scale well within one integration; across many integrations, those differences become the work.
- Ordering and duplicates. 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.
- Unrecoverable drops. Microsoft Graph retries a failed delivery for up to four hours and documents that dropped notifications can't be recovered; consumers who receive a
missedlifecycle event are directed to resync through a delta query. - Finite retry windows. 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.
- Native retries on Unified.to. A native webhook delivery that your endpoint doesn't acknowledge is retried three times immediately; 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.
- Payloads on Unified.to. Two things Unified.to does change what a consumer has to handle. Events carry the changed records where supported, so a follow-up fetch is usually not needed. 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, on supported integrations and objects, where the integration's 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 trial. 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.
- Changes are detected using timestamps and cursors. The read position only advances after a successful delivery to your endpoint.
- Unified.to stores no customer records at rest; between checks it keeps only the read position. If a delivery fails, Unified.to makes up to 3 immediate attempts 1 second apart, then backs off progressively for up to about two weeks, re-reading the same page from the integration on each retry rather than resending a stored payload. If retries are exhausted, the webhook is marked unhealthy and scheduling stops. See best practices for webhooks and data residency in regulated applications.
- Deleted records: native webhooks deliver deletes where the integration sends them. On virtual webhooks,
deletedevents are reliable only where the check detects deletions: BambooHR, Dropbox, Google Drive, Gmail, JobAdder, OneDrive, SharePoint, RingCentral, Egnyte and Box. Check the integration's Feature Support tab in the dashboard for what each object supports.
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. For a closer look at how virtual webhooks detect changes compared with polling jobs, see What Are Virtual Webhooks?
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. If you're moving an existing polling setup over, migrating from polling to webhooks, step by step covers the cutover.
- 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 wherever initial sync is supported, 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?
Use a webhook where reaction time matters, polling where periodic completeness or an outbound-only setup matters more, and a hybrid with scheduled reconciliation where you need both speed and completeness; the table gives a default for common situations.
| 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 |
| AI agents or automations triggered by changes across customers' tools | Webhook, virtual where native isn't offered | Agents act on events; polling every connection wastes quota on empty checks |
| 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 event 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. Put another way, you're choosing which failure modes to own: distributed polling infrastructure, inconsistent behaviour across integrations, or one centralized layer whose health you monitor. |
How do latency and cost compare in numbers?
Polling latency averages half the interval and peaks at the full interval, while a virtual webhook checks at an interval you set, so a change arrives in half your interval on average and one interval at worst; 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; for virtual webhooks, the interval goes down to 1 minute on paid plans.
| Polling every 5 minutes | Polling every 60 seconds | Virtual webhook, 60-minute interval (trial) | Virtual webhook, 1-minute interval (paid) | Native webhook | |
|---|---|---|---|---|---|
| Average delay | 2.5 minutes | 30 seconds | Half your interval | Half your interval | Integration-dependent |
| Worst case | 5 minutes | 60 seconds | One interval | One interval | 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. Because empty checks still count against the integration's rate limit, set the interval to how quickly your product has to react, and no shorter. |
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. For a walkthrough, see How to create and configure webhooks; for everything a subscription covers, from initial sync to billing, see what a Unified.to webhook subscription does.
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 some 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 (every payload field is documented in the webhooks reference):
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 I switch a webhook from virtual to native later? Not in place. The type is set at creation and can't be changed with an 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.
→ Start your 30-day free trial
Written by Mallory Greene.
About the author: Mallory Greene is Head of Marketing at Unified.to. She writes about integration infrastructure, unified APIs, and MCP for technical teams. Based in Toronto.