Workflow automation

Webhooks vs Polling: Picking the Right Trigger

One gives you latency, the other gives you truth, and production systems need both. The delivery guarantees, the boundary bugs, and the arithmetic that sets your poll interval.

On this page
  1. The short answer
  2. What each mechanism actually guarantees
  3. The pattern that makes the choice stop mattering
  4. Receiving a webhook without losing events
  5. Polling without silently missing rows
  6. The arithmetic that sets your poll interval
  7. How to choose for a given integration
  8. Where it goes wrong
  9. Definitions

The short answer

The short answer

Use a webhook for latency and a poll for truth, and build both. A webhook tells you something changed within seconds but offers at-least-once delivery, no ordering, and silence when your endpoint was down. A reconciliation poll is slower but it can rebuild your entire state from nothing. Integrations that carry only one of the two either run minutes behind or drift quietly out of sync, and the second failure is much harder to see because nothing ever errors.

The framing as a versus question is what causes the damage. Teams pick one, defend the choice, and inherit exactly the failure mode the other one covers. A webhook only system loses every event that arrived during a deploy longer than the provider's retry window. A polling only system is either expensive or slow, and on most APIs it still cannot see a record that was created and deleted between two polls.

  • At-least-onceAssume every webhook is delivered more than once and design the handler to be safe when it is.
  • Raw bytesVerify the signature against the exact body received, before any JSON parsing touches it.
  • Ack, then workReturn 200 inside the provider's timeout and do the work in a queue the request never waits on.
  • Thin payloadTreat the event as an id and a nudge, then re-read the object from the API for current state.
  • Sweep gapHow much work your reconciliation poll finds is the honest measure of how much the webhook is missing.

This is integration plumbing rather than anything clever, and it is where most of the reliability in an automation actually comes from. It is the bulk of what systems integration work involves.

What each mechanism actually guarantees

Compare the guarantees rather than the convenience, because the guarantees are what you will be debugging at the end of the quarter.

Webhook (push)Polling (pull)
LatencySeconds, usuallyHalf the poll interval on average, the full interval at worst
Delivery guaranteeAt-least-once while the provider's retry window lasts, then nothingWhatever the query returns, every time you ask
OrderingNone. Retries routinely arrive after newer eventsWhatever you sort by, under your control
Behaviour when you are downProvider retries for a while, then gives up and may disable the endpointYou catch up on the next run, with no data loss
Cost shapeScales with the number of real changesScales with interval times number of objects, whether or not anything changed
Can it rebuild state from zeroNo. It only tells you about the futureYes, and that is the entire point of keeping it
Main failure modeSilent loss during an outage or a deployMissed rows at the query boundary, and rate limits

Two more options exist and get forgotten. Provider delta endpoints, such as a change feed with a cursor, give you polling economics with webhook completeness, at the price of a cursor that expires and forces a full resync. Change data capture, reading the database log directly, gives ordered and complete streams when you own the database, and is the correct answer for internal systems far more often than people expect.

The pattern that makes the choice stop mattering

Run both paths into one handler and treat the webhook as an optimisation over the poll, not as the mechanism. This costs one extra scheduled job and removes the entire category of incident where an integration was silently a day behind.

Framework

Nudge and Sweep

Every integration gets a nudge that optimises latency and a sweep that guarantees truth. Five rules make them safe to run together.

01
The nudge carries an id, not a state

Take the resource id and the event type from the payload and throw the rest away. Then read the object from the API. A payload built four minutes ago that arrives after two retries will tell you about a state that no longer exists, and acting on it overwrites newer data with older data.

02
The sweep is authoritative

The reconciliation poll must be able to reconstruct your state from nothing, without a single webhook ever having fired. If it cannot, you do not have a backstop, you have a second partial feed. Test this by running the sweep against an empty database once, deliberately.

03
The sweep window is wider than the retry window

Set the lookback longer than the provider's total retry period plus your longest plausible outage. If the provider retries for twenty four hours, a sweep covering the last hour is decorative. Overlap costs almost nothing because the handler is idempotent.

04
Both paths converge on one idempotent handler

The webhook worker and the sweep call the same function with the same resource id, and that function is safe to run any number of times. Two code paths that both write is how you get records that flip between two states depending on which arrived last.

05
Alert on what the sweep found

Count the records the sweep had to fix that the nudge never delivered. That number is your integration health metric, and it is the only one that detects a webhook subscription which quietly stopped firing. Nothing else in your monitoring will notice, because an event that never arrives produces no error, no latency spike and no log line.

The metric nobody instruments

Sweep-found work is the single most useful number in an integration. Zero for weeks then twelve today means something changed on the provider's side. A slow steady climb means your endpoint is intermittently failing and the retries are expiring. Both are invisible on a dashboard of successful webhook deliveries.

Receiving a webhook without losing events

The receiver has one job, which is to prove the message is genuine, claim it once, and acknowledge inside the provider's timeout. Every additional thing you do in that request handler is a way to lose events, because providers disable endpoints that fail repeatedly and most of them do it without a conversation.

Webhook receiver skeleton, the parts people get wrongpython
# receiver.py
# The whole job of this file: prove the message is real, claim it once, ack fast.
# Everything else happens in a worker that this file never waits for.

import hmac, hashlib, time, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET    = os.environ["WEBHOOK_SECRET"].encode()
TOLERANCE = 300          # seconds. Older than this is a replay, not a late delivery.

@app.post("/hooks/vendor")
def receive():
    raw = request.get_data()                       # BYTES, exactly as sent.
    # Never request.json here. Parsing and re-serialising changes key order and
    # whitespace, the recomputed HMAC stops matching, and you will blame the vendor.

    sig = request.headers.get("X-Signature", "")
    ts  = request.headers.get("X-Timestamp", "0")

    if abs(time.time() - int(ts)) > TOLERANCE:
        abort(400, "stale timestamp")              # replay protection

    expected = hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):     # constant time. Not ==.
        abort(401)

    event_id = request.headers.get("X-Event-Id") or hashlib.sha256(raw).hexdigest()

    if not claim(event_id):        # INSERT ... ON CONFLICT DO NOTHING, returns rowcount>0
        return "", 200             # already seen. At-least-once delivery is normal.

    enqueue({"event_id": event_id, "body": raw.decode(), "received_at": time.time()})
    return "", 200                 # inside the provider timeout, every time

# The rules this file exists to enforce:
#  1. No business logic, no third-party calls, no model calls, no waiting.
#  2. Return 5xx ONLY when you want the provider to retry. A validation failure
#     you will never accept is a 4xx, or the retry runs until the endpoint is disabled.
#  3. The payload is a notification, not evidence. The worker re-reads the object
#     from the API using the id, because the payload was true when it was built
#     and the object may have changed three times since.

The raw body rule deserves its own paragraph because it burns a day of somebody's life on nearly every integration. Signatures are computed over the exact bytes transmitted. Frameworks that parse JSON for you produce a dictionary, and serialising that dictionary back to a string changes key order, whitespace and unicode escaping. The recomputed hash then differs from the header, every request fails verification, and the natural conclusion is that the vendor's signing is broken. Capture the raw bytes before any middleware touches them.

  • Do the work asynchronously. If processing takes longer than the provider's timeout you get a retry for an event you are still handling, and now two workers are writing the same record. Ack, enqueue, return.
  • Choose your status codes deliberately. A 5xx means retry me. Return it only when a retry could succeed. A malformed payload you will never accept is a 4xx plus a log line, otherwise the provider retries a permanent failure until it disables your endpoint.
  • Handle bursts. A backfill, a bulk import or a provider incident can deliver a day of events in a minute. The queue absorbs that. A handler that writes directly to your database does not.
  • Log the delivery, not just the outcome. Store event id, received timestamp, signature result and queue offset before processing. When someone asks whether an event arrived at 14:32, the answer has to come from data rather than inference.
  • Rotate secrets without downtime. Accept both the old and new signing secret during the rotation window, then drop the old one. Providers rarely coordinate rotation with your deploy schedule.
Deploys are the most common cause of lost events

A rolling deploy that returns 502 for ninety seconds is fine if the provider retries. A migration that takes the endpoint down for twenty minutes may exceed a short retry window, and those events are gone with no error anywhere in your system. Either keep the receiver available independently of the rest of the application, or accept that the sweep is what recovers them. Most teams discover which one they chose after the fact.

Polling without silently missing rows

Polling looks simple and has one classic bug that is nearly invisible in testing. You store the maximum updated_at you have seen and next time ask for everything strictly greater. Any record written in the same second as your high water mark, after your query ran, is never returned again. It is not deleted, not errored, and not missing from any report you look at. It is just absent.

StrategyWhat it missesCostUse it when
Offset pagination (page=1,2,3)Rows shift between pages while you read, so you skip and duplicateLowNever on a mutating dataset. It is only safe on a frozen export
Timestamp cursor, strictly greaterAnything written in the same second, plus everything during clock skewLowNever on its own. This is the bug described above
Timestamp cursor with overlapVery little, if the overlap exceeds skew and write latencyLow, plus reprocessing the overlapMost REST APIs. Overlap by minutes and rely on idempotency
Keyset pagination on a stable sortNothing, as long as the sort key is immutable and uniqueLowAny API that offers it. This is the correct default
Provider delta or change feedNothing, until the cursor expires and forces a full resyncLowest per changeAvailable on major platform APIs. Build the resync path first
Change data capture on the database logNothing. Ordered and completeOperationally heavierInternal systems where you own the database
Six ways to ask what changed. Two of them lose rows by construction.

Three more polling traps are worth naming. Soft deletes are invisible to a query filtered on updated_at unless the provider bumps that field on delete, so a record that vanished from the source stays in your store forever. Clock skew means the provider's timestamps and yours disagree, which is why the overlap window exists at all. And hard deletes leave no trace whatsoever, so periodic full key comparison, not just an incremental poll, is the only way to detect them.

Overlap is only safe if the handler is idempotent

Reprocessing the overlap window means the same record is handled repeatedly, on purpose, several times an hour. If that sends an email, charges a card or appends a row, the overlap turns a completeness fix into a duplication incident. Get idempotency right first, as in idempotency in automation, then widen the window freely.

The arithmetic that sets your poll interval

Poll frequency is a budget decision with three inputs: how stale the data may be, how much of the rate limit you are willing to spend, and how many requests you are prepared to waste on nothing having changed. Work it out rather than picking five minutes because it sounds sensible.

Polling cost and staleness

Enter your own figures. Rate limits vary by provider, by plan and over time, so read your current limit from the provider's documentation or response headers and put that number in rather than trusting any figure written down elsewhere.

0Poll requests per day
0Requests spent per real change
0Average staleness in seconds
0Percent of your rate limit consumed by polling alone

Two readings of that output matter. A high requests per change figure is the honest argument for adding a webhook, because you are paying for silence. And the rate limit percentage is what you have left for everything else: user triggered actions, retries and the backfill you will run one day. Polling that consumes most of the allowance in steady state leaves nothing for the hour you actually need headroom, which is covered in rate limits, retries and backoff.

One scheduling detail saves real incidents. Do not start every poll on the minute. Ten integrations all firing at 00:00 create a spike that trips your own limits and the provider's, and it recurs at the same instant every hour so it looks like a mysterious periodic failure. Add a fixed per integration offset and a small jitter.

How to choose for a given integration

Pick the primary mechanism from the situation, then add the other one as the backstop. The primary decides your latency and your cost. The backstop decides whether you find out when the primary fails.

SituationPrimaryBackstopWhy
A person is waiting on screen for the resultWebhookHourly sweepSeconds matter and the volume of real changes is low
Payments, orders, anything with money attachedWebhookFrequent sweep, plus daily full comparisonLoss is unacceptable and duplicates are worse, so idempotency and reconciliation both carry weight
The provider offers no webhooksKeyset or delta pollPeriodic full key comparisonThe only question left is how to poll without missing rows
Nightly batch feeding a reportPollNone neededLatency is irrelevant, so a single scheduled run beats an endpoint you have to keep available
Provider webhooks are known to be unreliablePollWebhook as an accelerator onlyInvert the roles rather than arguing with the vendor
High volume internal system you ownChange data captureSweep on a slow scheduleOrdered, complete, and it does not consume anyone's API quota
Bulk import or migrationPoll a job status endpointNothingWebhooks add fragility to a one-off operation with a human watching it
The backstop column is the one that usually gets cut and should not be.
Before this integration goes live
0 of 12 done

Where it goes wrong

Assuming events arrive in order

A create retried twice can land after the update that followed it. If your handler applies whatever arrived last, the record ends up in the older state and stays there. The fix is to compare a version, a sequence number or the object's own updated timestamp, and to ignore any event describing a state older than what you already hold. Re-reading the object instead of trusting the payload solves this for free, which is the strongest argument for thin payloads.

Treating a disabled endpoint as a vendor problem

Providers commonly disable subscriptions after a run of consecutive failures, and the notification goes to whichever address was on the integration account. That is frequently a shared mailbox nobody reads. The integration then works perfectly and receives nothing, indefinitely. Sweep-found work is what detects this, usually days before anyone finds the email.

Retry storms after an outage

When your endpoint recovers, the provider delivers everything it has been holding, and your queue depth goes vertical while your workers hammer a downstream API that has its own rate limit. Cap worker concurrency, back off on 429 responses with the provider's Retry-After header rather than a fixed sleep, and let the queue stay deep for a while. Failing slowly is a design choice, and it is covered further in automation error handling.

Verifying nothing at all

An unauthenticated webhook endpoint is an unauthenticated write API on your production system, and its URL appears in browser network tabs, vendor dashboards and support tickets. If a provider offers no signing at all, put a shared secret in the path, restrict by source address where the provider publishes ranges, and treat everything the payload says as a hint to be verified by reading the real object.

Definitions

Terms used precisely on this page
Webhook
An HTTP request sent by a provider to a URL you registered, telling you that something changed on their side. Delivery is typically at-least-once within a retry window, unordered, and permanently lost once that window expires.
Polling
A scheduled request from your system asking a provider what has changed since a stored cursor. It trades latency and request volume for the ability to reconstruct state at any time without depending on anything having been delivered.
At-least-once delivery
A guarantee that a message will be delivered one or more times, never zero within the retry window, and with no upper bound on duplicates. It obliges the receiver to make repeated processing of the same message harmless.
Reconciliation sweep
A periodic pass that compares your stored state against the provider's current state over a window wider than the provider's retry period, correcting differences. The volume of work it finds measures how much the push path is missing.
Keyset pagination
Paging through results using the last seen value of a stable, unique, immutable sort key rather than a numeric offset, so that rows inserted or updated during the read cannot cause pages to skip or repeat records.
Thin payload
A webhook design in which the message carries only a resource identifier and an event type, and the receiver reads current state from the API. It removes staleness and ordering problems at the cost of one extra request per event.

Questions readers ask next

Do I really need polling if the provider's webhooks are reliable?
Yes, at a low frequency. Reliability is not the question, because the failure you are covering is on your side: a deploy, an outage, a disabled subscription or a bug that acknowledged an event and dropped it. An hourly or nightly sweep costs very little and converts a silent multi-day drift into a correction nobody has to notice.
Why does my signature verification fail even though the secret is correct?
Almost always because you are hashing a re-serialised body rather than the raw bytes received. A framework that parses JSON and hands you an object has already changed key order and whitespace, so the recomputed hash cannot match. Capture the raw body before middleware runs, and also confirm exactly which parts the provider signs, since many prepend a timestamp.
How long should the reconciliation sweep look back?
Longer than the provider's total retry period plus the longest outage you consider plausible, which in practice means at least twenty four hours for anything important. Overlap is cheap when your handler is idempotent, and the cost of a window that is too short only becomes visible when data has already been missing for days.
Should webhook payloads be trusted for the object's current state?
No. Treat the payload as a notification carrying an id and an event type, then read the object from the API. A retried delivery describes state that may be several changes old, and applying it will overwrite newer data with older data. Reading current state also removes most ordering problems without any version tracking.
What poll interval should I use?
Derive it from acceptable staleness and your rate limit rather than choosing a familiar number. Average staleness is half the interval, and requests per day is the number of endpoints times 86400 divided by the interval. Run the calculator on this page, then check what fraction of your allowance is left for user triggered actions and retries.
Is change data capture worth the operational overhead?
For internal systems where you own the database, frequently yes, because it gives ordered and complete change streams without consuming any API quota and without a boundary bug. For third party SaaS you have no such option, so the realistic choice is a delta endpoint where one exists and keyset polling where it does not.
Cite this

ChatGPTalker. "Webhooks vs Polling: How to Pick the Right Trigger." chatgptalker.com, 2026-08-26. https://chatgptalker.com/guides/webhooks-vs-polling/

Rather have it built than read about it?

Send the process you want automated. You get a scoped plan back, with the build shape, the stack and a realistic timeline.

Start a project