Short answer
Use webhooks for low-latency notification and scheduled polling for completeness. Shopify documents that webhooks can be delayed, duplicated, delivered out of order, and that subscriptions are removed after persistent failures, so it recommends reconciliation jobs. In practice, a Shopify–ERP integration should process webhooks as triggers and run an updated_at-based polling job (or bulk operation for large datasets) to catch anything missed.
- Webhooks Fast
- Polling job updated_at checkpoint
- Job queue Deduplicate by resource
- Worker Fetch current state
- ERP
Both paths feed the same idempotent job queue, so an order found by both is processed once.
“Should we use webhooks or polling?” is the wrong question for a Shopify ERP integration. Shopify’s own guidance answers it: use webhooks, and don’t rely on them alone. This guide explains the reasoning in Shopify-specific terms and shows how to build the hybrid. For a platform-neutral decision framework, see the webhooks vs. polling comparison.
What each approach gives you on Shopify
| Property | Webhooks | Polling the Admin API |
|---|---|---|
| Latency | Seconds, typically | Up to one polling interval |
| Completeness | Not guaranteed; subscription can be removed after repeated failures | Complete for the window queried, if the checkpoint logic is correct |
| Ordering | Not guaranteed | You control sort order (for example sortKey: UPDATED_AT) |
| Duplicates | Possible; deduplicate with X-Shopify-Webhook-Id |
Overlapping windows return the same records; deduplicate by resource ID and updatedAt |
| Rate-limit cost | None for delivery; API cost only when you fetch details | Every poll costs query points |
| Infrastructure | Public HTTPS endpoint, or EventBridge / Pub/Sub | Scheduler only |
The table shows why the two complement each other: webhooks are fast and cheap but incomplete; polling is complete but slower and costs API capacity.
The hybrid pattern
- Webhook path. Subscribe to the topics you need. The receiver verifies, stores, and acknowledges. A worker fetches current state and processes it.
- Polling path. A scheduled job queries resources updated since a stored checkpoint. For each resource, it enqueues the same job the webhook path would.
- One idempotent queue. Both paths create jobs keyed by resource ID (plus
updatedAt, if you want to distinguish versions). A job that already exists or a version already processed is skipped.
Because the worker always fetches current state and writes idempotently, it does not matter which path found the change first, or whether both did.
Implementing the polling job
Checkpoint logic
Store a checkpoint per shop and resource type. On each run:
- Read the checkpoint
T. - Query resources with
updated_atgreater thanT − overlap(a few minutes of overlap absorbs clock skew and in-flight writes). - Page through all results.
- Enqueue jobs for each resource.
- Only after every page succeeds, set the checkpoint to the highest
updatedAtseen (or the run’s start time).
If a run fails halfway, the checkpoint does not move and the next run repeats the window. Duplicates are harmless because the queue is idempotent.
async function pollUpdatedOrders(shop: string) {
const checkpoint = await checkpoints.get(shop, 'orders'); // e.g. 2026-10-26T10:00:00Z
const since = new Date(checkpoint.getTime() - 5 * 60_000).toISOString();
let cursor: string | null = null;
let maxSeen = checkpoint;
do {
const { data } = await shopify.graphql(RECENT_ORDERS, {
query: `updated_at:>'${since}'`,
cursor,
});
for (const o of data.orders.nodes) {
await jobs.enqueueUnique({ shop, type: 'order', resourceId: o.id, version: o.updatedAt });
if (new Date(o.updatedAt) > maxSeen) maxSeen = new Date(o.updatedAt);
}
cursor = data.orders.pageInfo.hasNextPage ? data.orders.pageInfo.endCursor : null;
} while (cursor);
await checkpoints.set(shop, 'orders', maxSeen);
}
query RecentOrders($query: String!, $cursor: String) {
orders(first: 100, after: $cursor, query: $query, sortKey: UPDATED_AT) {
pageInfo { hasNextPage endCursor }
nodes { id updatedAt }
}
}
Request only id and updatedAt in the poll. The worker fetches full details later; this keeps the polling query cheap.
Using bulk operations for large reconciliations
For full-catalog or full-inventory reconciliation, use bulkOperationRunQuery. Shopify runs the query asynchronously and gives you a URL to a JSONL file with all results. Subscribe to the bulk_operations/finish webhook topic (Shopify recommends this over polling the operation’s status) and process the file when it is ready. Bulk queries must complete within 10 days. Check the documentation for your API version on how many bulk operations can run concurrently — the limit changed in 2026-01.
Choosing the mix per workflow
| Workflow | Webhook topics | Polling or bulk |
|---|---|---|
| Order export | orders/create or orders/paid, orders/updated, orders/cancelled |
Poll orders by updated_at every 5–15 minutes |
| Refunds | refunds/create |
Covered by order polling if you compare refund lists |
| Catalog changes made in Shopify | products/update |
Nightly bulk export for drift checks |
| Inventory (ERP is source of truth) | Optional inventory_levels/update to detect manual edits |
Nightly bulk export of levels for reconciliation |
| App lifecycle | app/uninstalled |
Not applicable |
Rate-limit planning
Polling consumes the same cost-based budget as everything else. Keep it predictable:
- Poll narrow filters, not whole collections.
- Request minimal fields.
- Read the
extensions.cost.throttleStatusin responses and slow down when capacity is low. - Schedule heavy reconciliation outside peak sales hours.
Testing the hybrid
- Disable the webhook receiver for 30 minutes, place orders, re-enable it, and confirm the polling job exported every order exactly once.
- Let both paths find the same order and confirm a single ERP order.
- Kill the polling job mid-page and confirm the next run repeats the window without skipping records.
- Shift the server clock by a few minutes and confirm the overlap window still catches every order.
Common mistakes
- Advancing the checkpoint to “now” before processing finishes.
- Polling full order details instead of IDs, burning rate-limit capacity.
- Separate, non-idempotent queues for webhook and polling jobs.
- No overlap window, so orders updated during a run are missed.
Summary
On Shopify, webhooks and polling are not alternatives — they are two halves of a reliable design. Webhooks deliver speed; a checkpointed updated_at poll (or a bulk operation for large datasets) delivers completeness. Route both into one idempotent queue, fetch current state in the worker, and the integration stays correct even when either path has a bad day.
Frequently asked questions
Is polling Shopify bad practice?
Polling everything at high frequency wastes rate-limit capacity. Polling a narrow, filtered query such as orders updated since a checkpoint is a normal reconciliation technique and is what Shopify means by reconciliation jobs.
How often should I poll?
Match the interval to the maximum delay your business can accept for an event the webhook path missed. Every 5 to 15 minutes is common for orders; hourly or nightly is common for catalog and inventory reconciliation.
Can I use only polling and no webhooks?
Yes, and for some ERPs that process in batches it is a reasonable choice. You lose low latency and use more API capacity, but you remove the public webhook endpoint.
When should I use bulk operations instead of polling?
When a reconciliation needs large result sets, such as all inventory levels or all products. Bulk queries run asynchronously and return a downloadable JSONL file, and they avoid paging through large connections under rate limits.
Sources and further reading
Platform-specific statements on this page were checked against these sources on . Vendor APIs change; confirm details in the current documentation before you build.
- [1] Webhook best practices (reconciliation jobs, ordering) — Shopify
- [2] Troubleshooting webhooks (retries, removal) — Shopify
- [3] orders query (updated_at filter) — Shopify
- [4] Bulk operations (queries) — Shopify
- [5] Shopify API rate limits — Shopify
Related reading
- Comparisons Webhooks vs. Polling: Choosing How Your Integration Detects Changes Compare webhooks and polling for detecting changes in integrations — latency, completeness, cost, infrastructure, and failure modes — and learn when to combine them.
- Guides How to Handle Shopify Webhooks Reliably Build a Shopify webhook receiver that never loses events — HMAC verification, the five-second rule, deduplication, ordering, retries, and subscription removal.
- Concepts What Is Scheduled Synchronization? How scheduled, batch-based synchronization works in integrations, how to design checkpoints and windows, and where it fits alongside event-driven updates.
- Concepts What Are Webhooks? What webhooks are, how they deliver change notifications between systems, how to verify them, and the delivery behaviors integrations must be designed around.
- Guides How to Synchronize Shopify Orders with an ERP How to export Shopify orders to an ERP reliably — choosing the trigger, mapping lines, taxes, discounts and payments, handling edits, and avoiding duplicates.