How This Page Was Built

  • Evidence level: Editorial research.
  • This page is based on editorial research, source synthesis, and decision-support framing.
  • Use it to clarify fit, trade-offs, thresholds, and next steps before you act.

Start With This: Dedupe the side effect

Store one unique key per business action, then commit the write before you acknowledge the webhook. That order keeps a duplicate delivery from turning into a duplicate customer email, inventory decrement, or CRM update.

A practical baseline looks like this:

  • Save a delivery key if Shopify gives you a stable one in your logs.
  • Fall back to a composite business key, such as shop domain + topic + resource ID + version marker.
  • Put a uniqueness constraint on that key in your database or dedupe store.
  • Return a 2xx response only after the dedupe write and business write succeed.
  • Log the shop, topic, resource ID, and final processing result.

A webhook signature check does one job, it proves the request is authentic. It does not prove the request is unique. Two signed deliveries with the same payload still create duplicate side effects unless the handler blocks them.

Rule of thumb: if the duplicate cost is trivial, a narrow dedupe layer works. If the duplicate cost touches money, inventory, or customer communication, the handler needs both dedupe and idempotency.

How to Compare Your Options: Delivery ID, composite key, or queue

Use the lightest approach that stops duplicate side effects, then add durability only where retries and replays stay visible. The wrong choice is the one that looks simple but forces manual cleanup later.

Approach Best fit Strength Trade-off Maintenance burden
Delivery ID dedupe Short retry windows, low-risk events Simple and fast Breaks if retries outlive the retention window Low
Composite business key Orders, refunds, fulfillment, inventory Blocks duplicate side effects even when delivery metadata shifts Needs careful key design across topics Medium
Queue + idempotent worker Multiple downstream writes or slow processing Keeps the webhook endpoint thin and recoverable Adds queue retries, poison-message handling, and monitoring Medium to high
Event log + reconciliation High-volume syncs, backfills, audit-heavy flows Gives replay control and a clean audit trail More storage and more moving parts High

The maintenance burden matters more than the headline simplicity. A delivery-ID-only setup stays clean until replay jobs, worker crashes, or delayed retries push a second copy past the retention window. A queue-based design takes more setup, but it gives you a place to retry safely without re-running the business action.

If the app already uses a queue, do not let the queue become the dedupe layer by itself. The queue retries the job, not the side effect. The business write still needs its own uniqueness rule.

The Compromise to Understand: Exactly once is a system, not a setting

Design for at-least-once delivery and remove duplicate impact from every side effect. Shopify webhooks arrive as events, and the system around them decides whether the same event turns into one write or two.

The trade-off is straightforward. Simpler handlers stay easy to maintain, but they stop at the webhook boundary. Stronger handlers add a store, a unique constraint, and a reconciliation path. That extra state keeps duplicates from leaking into billing, inventory, or customer messaging.

The duplicate problem shows up at the second write, not the second webhook. If the handler writes to your database and then sends an email, the email needs its own idempotency rule or the process sends it twice after a retry.

A useful way to think about the compromise:

  • Notification-only events need a small dedupe key and a clean log.
  • State-changing events need a business key plus a transaction boundary.
  • Multi-system events need a queue, an idempotent worker, and reconciliation.

That is the real split between “simple enough” and “safe enough.”

What Changes the Answer: Order updates, refunds, and inventory

Match the dedupe key to the Shopify object, because the right rule changes by topic. One order can generate several valid webhooks, and those deliveries do not all deserve the same handling.

Event pattern Best dedupe rule Why it works
Order create and update Shop + order ID + version marker The order keeps changing, so new state should not collapse into old state
Refund or fulfillment Business event key plus state guard These actions have direct cost if they run twice
Inventory change Variant ID + quantity or version marker Stale replays distort counts quickly
Read-only analytics sync Delivery ID or batch marker Duplicate records matter less when the warehouse dedupes downstream
Replay or backfill job Durable business key plus long retention Old delivery IDs fall out of short retention windows

One order can touch several topics, and that is where many duplicate bugs start. A create webhook and an update webhook for the same order are not duplicates in the business sense. They are different state transitions that need separate handling, even if they point at the same record.

For inventory and fulfillment, the cost of a duplicate is higher than the cost of a little extra state. That is the point where a one-table dedupe cache stops being enough and a stronger write path pays off.

What to Verify Before You Commit: Retry, replay, and out-of-order delivery

Run five pressure checks before you lock the pattern. These checks expose the failures that simple happy-path logic misses.

Pressure check What should happen What a failure means
Same webhook twice before the first commit One business write The dedupe write sits in the wrong place
Retry after a worker crash One business write after recovery The queue or job runner lacks a stable idempotency key
Older update after newer update Latest state wins The handler trusts arrival order instead of resource version
Replay after the dedupe window Reconciliation catches it or a longer key blocks it Retention is too short for your replay pattern
Two topics touch the same order One downstream side effect per target system Keys are scoped too narrowly

This section answers a different question from the checklist. It is not about whether the logic looks right on paper. It is about whether the system stays correct when the same event arrives twice, late, or out of order.

If any one of those checks produces two side effects, the fix sits in one of three places: the key is too narrow, the write is not atomic, or the downstream system lacks its own idempotency rule.

Limits to Confirm: Retention, uniqueness, and transaction boundaries

Keep the dedupe record alive long enough to cover retries and replays. A 72-hour window covers common retry noise. A 7-day window fits replay-heavy systems and batch backfills.

The limit that breaks many setups is not the webhook itself, it is the mismatch between the dedupe store and the job lifecycle. A 24-hour TTL paired with a 7-day replay job creates a duplicate gap. That gap turns an old event into a fresh side effect.

Check these constraints before you trust the flow:

  • Dedupe storage lives longer than your retry and replay window.
  • The dedupe write and business write share one transaction or one atomic step.
  • Worker retries do not bypass the uniqueness constraint.
  • Any external API gets a stable idempotency key or a stable resource ID.
  • Multi-worker processing uses a unique constraint or lock, not just application logic.

If the webhook triggers a side effect outside your database, the downstream system needs a duplicate rule too. Local dedupe stops the first duplicate. It does not stop a second email provider, CRM, or fulfillment endpoint from acting twice.

Where This Advice Does Not Apply: Polling and append-only logs

Use a different route if the webhook is only a signal and the authoritative state lives elsewhere. In that setup, the webhook wakes a sync job, and the sync job fetches fresh Shopify state before writing anything important.

That pattern lowers the risk of duplicate side effects, but it does not remove the need for dedupe. It just moves the main responsibility from “do not write twice” to “do not trigger unnecessary work twice.”

A different route makes more sense in three cases:

  • The action is irreversible and has no stable business key, so the webhook should not trigger it directly.
  • The workflow needs strict ordering across multiple topics, and webhook arrival order does not guarantee that.
  • The system records an append-only audit trail, and duplicates are acceptable only when each record keeps a source key.

For those setups, a reconciliation job does more work than a simple inline handler, but it prevents duplicate customer impact and gives the team a recovery path.

Final Checks

Use this checklist before calling the setup done:

  • One stable key exists for each business action.
  • The dedupe store survives longer than the retry window.
  • The handler returns 2xx only after the write commits.
  • Every external side effect has its own idempotency rule.
  • Reordered updates have a version rule, not an arrival-order rule.
  • A reconciliation path exists for late, missed, or replayed events.
  • Duplicate spikes trigger an alert instead of a support ticket.

If any box stays empty, duplicates will leak through somewhere else in the flow. The fix belongs in the write path, not just in the webhook endpoint.

Common Mistakes to Avoid

Do not use the HMAC check as a dedupe rule. It confirms authenticity, not uniqueness.

Do not hash the full payload and call that a duplicate check. A real state change can keep most fields the same and still deserve to process.

Do not acknowledge the webhook before persistence. If the process dies after the 2xx response, Shopify stops retrying and the side effect is lost.

Do not dedupe only at the endpoint. If the downstream CRM, email service, or fulfillment tool lacks its own idempotency rule, the duplicate still happens there.

Do not let logs expire before the dedupe store. That gap removes the evidence you need to reconcile a late replay.

Do not treat every topic as the same event. Order updates, refunds, inventory changes, and notifications belong to different business rules.

The Practical Answer

For most setups, the best path is a durable dedupe store, an idempotent handler, and a reconciliation job for late or missed deliveries. That combination keeps the webhook layer simple without letting duplicate deliveries create duplicate business actions.

If the webhook touches money, inventory, fulfillment, or customer communication, choose the more durable path. Simplicity stops being a benefit when one duplicate creates cleanup work across systems.

Frequently Asked Questions

Should duplicate Shopify webhooks be ignored or processed?

Ignore the duplicate side effect, not necessarily the duplicate delivery. The handler should still record that the delivery arrived, then block the second write if the business action already completed.

Is the webhook ID enough to dedupe duplicates?

Use it when it stays stable in your delivery logs, then pair it with a business key for safety. Delivery IDs handle exact repeats. Business keys handle retries, replays, and multi-topic workflows tied to the same Shopify record.

Is HMAC verification enough to stop duplicates?

No. HMAC verification proves the request came from Shopify. It does not prove the request is new. A second valid delivery still passes HMAC and still needs dedupe.

How long should dedupe keys live?

Keep them long enough to cover your retry and replay pattern. A 72-hour window fits many event handlers. A 7-day window fits batch reprocessing and delayed reconciliation jobs.

What if two different Shopify topics hit the same order?

Process both, but guard the shared downstream side effect with a business key and a version rule. Two topics that touch the same order are not duplicate deliveries in the strict sense. They are separate state changes that need coordination.

Do I still need a queue if the handler is idempotent?

Yes, if the webhook triggers more than one write or any irreversible action. The queue protects the worker from crashes and spikes, while idempotency protects the business action from repeat delivery.

What is the biggest mistake with Shopify webhook duplicates?

Treating the webhook endpoint as the whole solution. The endpoint is only the entry point. The real protection sits in the unique key, the transaction boundary, and the downstream write rules.