Author picture Gareth Wilson

How to Handle Duplicate Shopify Webhook Events

Published


If you're building a Shopify app, you've likely encountered this scenario: your webhook handler processes an order, updates inventory, and sends a confirmation email, only to repeat everything moments later when the same webhook arrives again. Duplicate webhooks are a common challenge in Shopify development, and handling them incorrectly can lead to double charges, duplicate database entries, or customers receiving multiple notifications.

In this guide, we'll explain why Shopify sends duplicate webhooks, practical techniques to handle them, and how Hookdeck can simplify deduplication for your application.

Why Shopify sends duplicate webhooks

Shopify, like most webhook providers, operates on an "at-least-once" delivery model rather than "exactly-once" delivery. This means your application might receive the same event multiple times. Understanding why duplicates occur is the first step to handling them effectively.

Timeout and retry behavior

Shopify expects your endpoint to respond with a 200 OK status within 5 seconds. If your server doesn't acknowledge the webhook in time—whether due to slow processing, network latency, or server load—then Shopify assumes the delivery failed and schedules a retry.

Shopify retries failed webhooks up to 8 times over a 4-hour period using exponential backoff. During this window, network hiccups or temporary server issues can trigger multiple delivery attempts for the same event.

Multiple webhook subscriptions

A less obvious cause of duplicates is having multiple subscriptions for the same topic. This commonly happens with Shopify embedded apps when webhooks are defined in both shopify.app.toml and programmatically via shopifyApp(). Each subscription triggers a separate webhook delivery for every matching event. We've more about resolving that in our configuring Shopify webhook subscriptions article.

Shopify's infrastructure limitations

In rare cases, Shopify's own infrastructure may deliver the same webhook more than once. This isn't a bug but an intentional design choice that prioritizes reliable delivery over preventing duplicates.

Techniques for handling duplicate webhooks

Since duplicate webhooks are inevitable, your application needs to handle them gracefully. The goal is idempotent processing: ensuring that processing the same event multiple times produces the same result as processing it once.

Use the X-Shopify-Event-Id header

Shopify includes a unique identifier in the X-Shopify-Event-Id header with every webhook. This ID remains consistent across retry attempts, making it the most reliable way to detect duplicates.

Here's how to implement deduplication using this header:

const processedEvents = new Map(); // In production, use Redis or a database

app.post("/webhooks/orders/create", async (req, res) => {
  const eventId = req.headers["x-shopify-event-id"];

  // Check if we've already processed this event
  if (processedEvents.has(eventId)) {
    console.log(`Duplicate event ${eventId}, skipping`);
    return res.status(200).send("OK");
  }

  // Mark as processed before handling (prevents race conditions)
  processedEvents.set(eventId, Date.now());

  // Respond immediately to avoid timeout retries
  res.status(200).send("OK");

  // Process the webhook asynchronously
  try {
    await handleOrderCreated(req.body);
  } catch (error) {
    console.error(`Failed to process event ${eventId}:`, error);
    // Consider implementing a retry mechanism here
  }
});

HTTP headers are case-insensitive. Your application might receive X-Shopify-Event-Id, x-shopify-event-id, or any other casing. Normalize header names when accessing them.

Respond first, process later

One of the most effective ways to prevent duplicate deliveries is to respond to Shopify immediately, then process the webhook asynchronously. This approach has two benefits:

  1. It prevents timeout-triggered retries by responding within Shopify's 5-second window
  2. It decouples your response time from your processing time

Use persistent storage for event IDs

For production applications, store processed event IDs in a persistent datastore rather than in-memory. Redis is an excellent choice for this because of its speed and built-in TTL support:

const Redis = require("ioredis");
const redis = new Redis();

async function isEventProcessed(eventId) {
  const result = await redis.setnx(`shopify:event:${eventId}`, "1");
  if (result === 1) {
    // First time seeing this event
    await redis.expire(`shopify:event:${eventId}`, 14400); // 4-hour TTL
    return false;
  }
  return true;
}

app.post("/webhooks/orders/create", async (req, res) => {
  const eventId = req.headers["x-shopify-event-id"];

  if (await isEventProcessed(eventId)) {
    return res.status(200).send("OK");
  }

  res.status(200).send("OK");
  await handleOrderCreated(req.body);
});

The TTL should match or exceed Shopify's 4-hour retry window to ensure you catch all possible duplicates.

Make your processing idempotent

Beyond detecting duplicates at the webhook level, design your business logic to be idempotent. This provides defense in depth:

async function handleOrderCreated(orderData) {
  // Use upsert instead of insert
  await db.orders.upsert({
    where: { shopifyOrderId: orderData.id },
    create: {
      shopifyOrderId: orderData.id,
      ...orderData,
    },
    update: {}, // Don't overwrite if exists
  });

  // Check before sending notifications
  const notificationSent = await db.notifications.findFirst({
    where: { orderId: orderData.id, type: "ORDER_CONFIRMATION" },
  });

  if (!notificationSent) {
    await sendOrderConfirmation(orderData);
    await db.notifications.create({
      data: { orderId: orderData.id, type: "ORDER_CONFIRMATION" },
    });
  }
}

How Hookdeck simplifies webhook deduplication

While building deduplication into your application is possible, it adds complexity and requires maintaining additional infrastructure. Hookdeck provides built-in deduplication at the event gateway level, handling duplicates before they reach your application.

What is Hookdeck?

Hookdeck provides an event gateway that sits between webhook sources (like Shopify) and your application. It enables webhook reliability with features like queuing, retries, rate limiting, and deduplication.

Setting up deduplication with Hookdeck

With Hookdeck, you point Shopify's webhooks to a Hookdeck URL, configure your deduplication rules, and let Hookdeck forward deduplicated events to your application.

Create a connection

In the Hookdeck dashboard, create a connection with Shopify as the source type. Hookdeck automatically configures signature verification and other Shopify-specific settings.

Configure deduplication rules

Add a deduplication rule that uses the X-Shopify-Event-Id header as the key:

{
  "type": "deduplicate",
  "window": 300000,
  "include_fields": ["headers.x-shopify-event-id"]
}

This configuration drops any webhook with the same event ID within a 5-minute window (300,000 milliseconds).

Update your Shopify webhook URLs

Replace your application's webhook URLs with the Hookdeck source URL. Hookdeck handles signature verification, deduplication, and delivery to your actual endpoint.

Deduplication strategies

Hookdeck offers flexible deduplication strategies for different scenarios:

Event ID deduplication: Use when Shopify retries the same webhook multiple times:

{
  "type": "deduplicate",
  "window": 300000,
  "include_fields": ["headers.x-shopify-event-id"]
}

Content-based deduplication: Use when multiple webhook subscriptions send identical payloads:

{
  "type": "deduplicate",
  "window": 300000,
  "exclude_fields": [
    "headers.x-shopify-webhook-id",
    "headers.x-shopify-event-id",
    "headers.x-shopify-triggered-at",
    "headers.x-shopify-hmac-sha256"
  ]
}

By excluding headers that differ between deliveries, you deduplicate based on the actual event content.

Noisy update filtering: Use when you only care about specific field changes. For example, if you're syncing products but don't need inventory updates:

{
  "type": "deduplicate",
  "window": 300000,
  "exclude_fields": ["body.updated_at", "body.inventory_quantity"]
}

Additional Hookdeck benefits

Beyond deduplication, Hookdeck provides:

  • Automatic retries: If your endpoint fails, Hookdeck retries with configurable backoff
  • Rate limiting: Protect your application from webhook spikes
  • Transformations: Modify payloads before delivery using JavaScript
  • Observability: Monitor webhook delivery, latency, and failures in real-time
  • Local development: Route webhooks to localhost during development

Conclusion

Duplicate Shopify webhooks are an inevitable part of building reliable integrations. By understanding why duplicates occur and implementing proper handling, whether through application-level deduplication or infrastructure tools like Hookdeck, then you can build robust webhook handlers that process each event exactly once.

Additional resources


Author picture

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.