How to Handle Duplicate Shopify Webhook Events
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:
- It prevents timeout-triggered retries by responding within Shopify's 5-second window
- 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.