Integration pattern
Reviewed 14 October 2026
Wait for a webhook, or ask every minute? Count what each costs.webhook vs polling
A webhook costs one request per change. A poll costs the same every day, changed or not. Set volume and delay to see which wins.
No demo shows a webhook or a poll. The plotter below is the demo.
Use webhooks when
- Changes are rarer than the polls that would catch them: under 1,440 a day if a minute is the longest wait you accept.
- The source offers webhooks and can reach an address of yours.
- A change matters within seconds, not hours.
Poll instead when
- The source sends no webhooks, or cannot reach you: a desktop system, a private network.
- Changes come faster than you would poll and a wait of minutes is fine: one poll then collects many.
- A missed change would cost money. Keep a poll, alone or as the net beside webhooks.
Try it
Cost and delay plotter
Set the changes a day and the longest wait you accept. See the requests a day each way, the break-even and a verdict. Sample data. Not a measurement.
10 · 100 · 1,000 · 10,000 · 100,000
The longest wait you accept
Does the source send webhooks?
Can the source reach your system?
A desktop program or a private network cannot be reached.Not used: the source sends no webhooks.
What a missed change costs
Sample: one poll request returns up to 100 changes. Frappe's REST list returns 20 unless you ask for more.
Requests a day against the time between polls
Counted, not measured. The same numbers are in the table under the tool.
1,000 requests a day against 1,440 for polling every minute. Money or stock moves, and delivery is not guaranteed, so keep a slower poll as the net.
- Webhooks
- 1,000 requests a day, one for each change
- Polling every minute
- 1,440 requests a day, at least 440 of them with nothing new
- Break-even
- Polling every 1.4 minutes costs what 1,000 webhooks cost
- Wait for a change
- Webhook: set by the sender. Poll: up to 1 minute, 30 seconds on average
The same numbers as a table
| Time between polls | Polling, requests a day | Webhooks, requests a day |
|---|---|---|
| 10 seconds | 8,640 | 1,000 |
| 1 minute your wait | 1,440 | 1,000 |
| 15 minutes | 96 | 1,000 |
| 1 hour | 24 | 1,000 |
| Next morning | 10 | 1,000 |
Counted, not measured. 1,000 changes a day; one poll request returns up to 100 changes.
What you give up, and how it fails.
What you give up
| What you get | What it costs |
|---|---|
| A webhook sends one request per change and none when nothing changed. | You run a public address, check each signature and handle repeats and retries. |
| The change arrives when the sender sends it, often within seconds. | You do not choose the delay or the order, and the sender stops retrying after a fixed time. |
| A poll lets you choose the interval, the batch and the order. | Every poll costs a request and a slice of the rate limit, whether or not anything changed. |
| A poll needs no public address and, with a saved checkpoint, does not skip a change it can see. | It cannot see a deletion unless the source lists deletions, and two changes inside one interval look like one. |
How it fails
| What happens | Why | Evidence |
|---|---|---|
| Webhooks stop arriving and nothing says so. | Shopify says delivery isn't always guaranteed and deletes an API-created subscription after eight failures in a row. Razorpay disables a webhook that fails for 24 hours. Stripe retries for up to three days; after that only its List events API, which reaches back 30 days, still holds the event. | |
| Events arrive out of order. | Shopify, Stripe and Razorpay all say order is not guaranteed. Fetch the current record instead of trusting the order of arrival. | |
| The poll runs less often than you set. | A Frappe scheduled job of type All runs every four minutes by default, and the Shopify connector's 5-minute setting is a minimum gap checked on those ticks. Version-16's scheduler wakes every four minutes unless scheduler_tick_interval is set; version-15's wakes every minute. | |
| The poll never runs. | Frappe queues no scheduled job while the scheduler is paused or disabled. Medusa runs scheduled jobs only on the worker, so a deployment with only a server instance runs none. | |
| A poll by modified date cannot see a deletion. | A deleted row is simply missing from the list. Frappe records each deletion in Deleted Document, so read that list as well. | DocumentedFrappe v15 · Deleted Document |
| A rate limit can count what you poll. | A Frappe site can cap server time per window with the rate_limit setting; a poll over the cap is refused with 429 and a Retry-After header. Polling every ten seconds is 8,640 requests a day even if nothing changes. | DocumentedFrappe v15 · rate limiter |
Each failure is Documented, with the maker's own page or code, or it Follows from the design. None is a result from a MithTech client project.
In ERPNext and Frappe, and Medusa.
Read from Frappe v15.123.0 and v16.52.0, ERPNext v15.122.0, Frappe's Ecommerce Integrations app (version-15, commit 3dc8eb81) and Medusa v2.21.2 on 14 October 2026, with the file and line. Other systems' names and settings were not checked here.
Where each part lives
| Feature | Where | What it does |
|---|---|---|
| Send a change (webhook) | Sends a document's event to a URL after the save commits: after insert, on update, on submit, on cancel, on trash or on change. A condition can filter it. The pending send waits in the request's memory, then a background job.Frappe v15 · webhook queueFrappe v15 · commit hooks | |
| Retries and log | Three tries, five seconds each by default. A timeout is retried at once, other errors after one and four seconds. One log row per attempt.Frappe v15 · webhook send | |
| Poll a doctype | Filter on modified, sort on it and page through. The list returns 20 rows unless you ask for more, and modified is indexed on every doctype.Frappe v15 · REST listFrappe v15 · modified index | |
| See deletions | One row for each deleted document, with its JSON, unless the delete was permanent.Frappe v15 · Deleted Document | |
| Poll from inside Frappe | , frequency | Runs every four minutes by default in version-15 and version-16. The scheduler wakes every minute in version-15 and every four minutes in version-16, unless scheduler_tick_interval is set.Frappe · scheduled job typeFrappe v15 · schedulerFrappe v16 · scheduler |
| Limit the calls | site config | Off unless set. It counts server time, not requests. Over the limit: 429 with Retry-After.Frappe v15 · rate limiter |
| Both together | Ecommerce Integrations › Shopify Setting | Orders arrive by Shopify webhooks; the connector saves the payload in an Ecommerce Integration Log, commits, then queues the job. Stock goes out on a schedule, only for items whose Bin changed since the last sync.Ecommerce Integrations · webhook topicsEcommerce Integrations · Shopify receiverEcommerce Integrations · logEcommerce Integrations · changed BinsEcommerce Integrations · stock push |
| In Medusa | Event Module, subscribers, scheduled jobs | Subscribers react to events. The default Local module is for development; Redis is the production module and makes one attempt unless attempts is set. Scheduled jobs and subscribers run only on a worker instance.Medusa Event ModuleMedusa v2 · Redis event busMedusa worker mode |
How to build it
Write down two numbers: changes a day, and the longest wait you accept. Divide 86,400 by the changes: that many seconds between polls costs as much as the webhooks.
If the source offers webhooks and can reach you, take them. Save the payload, answer fast, do the work after.
Add a slow poll as the net: fetch what changed since the last run, and save the checkpoint only after the work is done.
If the source sends nothing or cannot reach you, poll. Save a checkpoint, page through, and back off when told to wait.
In Frappe, filter the REST list on modified, sort on it, raise the page size above 20 and read Deleted Document for deletions. For a webhook, add a condition so only the changes you need are sent.