Shopify sends webhooks the moment something important happens in a store. An order gets placed, a product changes, a customer updates their profile, and your app needs to know right away. But webhooks fail. Networks drop, servers time out, deploys happen mid-request, and suddenly your app misses a critical event.
This is where retry strategies matter. A well-designed retry system turns transient failures into non-events. A poorly designed one either loses data silently or hammers your infrastructure into the ground. In this guide, we break down Shopify webhook retry strategies, the logic behind them, and the patterns that keep your integrations reliable at scale.
How Shopify Webhook Delivery Actually Works
Before you build your own retry logic, you need to understand what Shopify already does for you.
When Shopify fires a webhook, it sends an HTTP POST to your endpoint. It expects a 2xx response within a few seconds. If your endpoint returns anything else, or times out, Shopify treats the delivery as failed.
Shopify then retries automatically. It attempts delivery over a 48-hour window with increasing gaps between attempts. If every attempt fails, Shopify removes the webhook subscription and sends you a warning email.
Here is the key takeaway: Shopify’s retries are a safety net, not a strategy. You should never rely on them alone. Your own system needs its own retry layer because Shopify’s retries are coarse, limited, and will eventually give up.
For a deeper look at how event-driven delivery fits into store architecture, see our guide on Shopify webhooks and event-driven architecture.
Why Webhooks Fail in the First Place
Understanding failure modes shapes your retry design. Not every failure deserves the same response.
| Failure type | Example | Should you retry? |
|---|---|---|
| Transient network error | Connection reset, DNS blip | Yes, with backoff |
| Server overload | 503, timeout during traffic spike | Yes, with backoff |
| Deploy or restart | 502 during rolling deploy | Yes, short delay |
| Downstream outage | Database or CRM down | Yes, longer backoff |
| Validation failure | Bad HMAC signature | No, reject permanently |
| Logic bug | Null pointer in handler | No, fix the code |
The distinction between retryable and non-retryable errors is the foundation of good webhook retry logic. Retrying a permanent failure wastes resources and never succeeds. Failing to retry a transient error loses data.
The Core Principle: Accept Fast, Process Later
The single most important pattern for reliable webhooks is to separate acceptance from processing.
When Shopify sends a webhook, do the minimum work needed to validate and store it, then return 200 immediately. Do the real processing afterward in a background job.
This matters for two reasons. First, Shopify has a tight timeout. Heavy processing inside the request risks a timeout and a false failure. Second, if you process synchronously, any downstream error forces Shopify to retry the entire delivery, which you do not control.
A typical accept-fast handler looks like this:
public function handleWebhook(Request $request)
{
if (!$this->verifyHmac($request)) {
return response('Unauthorized', 401);
}
WebhookJob::dispatch(
$request->header('X-Shopify-Topic'),
$request->getContent(),
$request->header('X-Shopify-Webhook-Id')
);
return response('OK', 200);
}
This approach pairs naturally with queue-based webhook processing, where the queue becomes the home for all your retry logic. Our breakdown of async event processing architectures explains why this decoupling scales so well.
Exponential Backoff: The Default Retry Strategy
Once processing moves to a background worker, you own the retry schedule. The most widely used pattern is exponential backoff.
The idea is simple. After each failed attempt, you wait longer before the next one. Instead of retrying every second and overwhelming a struggling service, you give it room to recover.
A basic backoff schedule looks like this:
| Attempt | Delay before retry |
|---|---|
| 1 | 10 seconds |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
| 6 | 6 hours |
The delay grows roughly by a multiplier each time. Here is a simple implementation of exponential backoff in Shopify worker code:
public function backoff(int $attempt): int
{
$base = 10;
$delay = $base * (2 ** ($attempt - 1));
return min($delay, 21600);
}
The min cap prevents delays from growing to absurd lengths. You want retries to space out, not to disappear for days.
Exponential backoff solves the thundering herd problem. When a downstream service recovers from an outage, you do not want every pending webhook retrying at once. For high-volume stores, combine this with the techniques in our rate limit recovery strategies guide.
Add Jitter to Prevent Synchronized Retries
Pure exponential backoff has a hidden flaw. If a thousand webhooks fail at the same instant, they all retry at exactly the same future moment. That creates a retry stampede.
Jitter fixes this by adding randomness to each delay. Instead of every job retrying at exactly 30 seconds, each one retries somewhere between 20 and 40 seconds.
public function backoffWithJitter(int $attempt): int
{
$base = 10;
$delay = min($base * (2 ** ($attempt - 1)), 21600);
$jitter = rand(0, (int)($delay * 0.3));
return $delay + $jitter;
}
Jitter spreads the load evenly across time. This small change makes a large difference when you are dealing with scaling webhook infrastructure under heavy traffic.
Idempotency: The Rule That Makes Retries Safe
Retries create a dangerous side effect. If a webhook partially succeeds before failing, a retry might run the same logic twice. That means duplicate orders, double emails, or corrupted inventory counts.
The solution is idempotency. Every webhook carries an X-Shopify-Webhook-Id header. Store it, and before processing, check whether you have already handled that ID.
public function process(string $webhookId, array $payload)
{
if (ProcessedWebhook::where('webhook_id', $webhookId)->exists()) {
return;
}
DB::transaction(function () use ($webhookId, $payload) {
$this->applyChanges($payload);
ProcessedWebhook::create(['webhook_id' => $webhookId]);
});
}
Idempotency is non-negotiable once you add retries. Our deep dive on idempotency strategies in Shopify systems covers edge cases like partial writes and concurrent retries. You should also review how to handle duplicate Shopify webhook events, since Shopify itself sometimes sends the same event more than once.
Handling Event Ordering During Retries
Retries break ordering. Imagine an order is created, then updated. If the create webhook fails and retries while the update succeeds first, your system processes events out of sequence.
This is a real problem for inventory and fulfillment logic. A few patterns help.
Use the updated_at timestamp inside the payload to detect stale events. If an incoming webhook is older than the data you already have, discard it. You can also version records and reject updates that arrive late.
For the full set of techniques, read our guide on ordering problems in Shopify webhooks. These patterns also help you avoid race conditions in Shopify order processing, which retries can amplify.
Dead Letter Queues: Where Failed Webhooks Go
No retry strategy succeeds forever. After a maximum number of attempts, a webhook needs to stop retrying and move somewhere safe. That place is a dead letter queue.
A dead letter queue, or DLQ, holds webhooks that exhausted their retries. Instead of losing them, you park them for later inspection or manual replay.
Here is a reasonable retry ceiling before moving to the DLQ:
| Setting | Recommended value |
|---|---|
| Max retry attempts | 6 to 8 |
| Total retry window | 24 to 48 hours |
| DLQ retention | 7 to 30 days |
| Alert threshold | Any DLQ entry |
A DLQ turns silent data loss into a visible, recoverable problem. Our guide on dead letter queues for Shopify webhooks walks through implementation in detail, and the broader retry architecture for Shopify failures article shows how the DLQ fits into the full pipeline.
Building a Replay System
A dead letter queue is only useful if you can replay what lands in it. Once you fix the bug or the downstream service recovers, you need to reprocess failed webhooks.
A replay system reads entries from the DLQ and feeds them back into your normal processing pipeline. Because your handlers are idempotent, replaying is safe even if some events already succeeded.
public function replay(string $dlqEntryId)
{
$entry = DeadLetter::findOrFail($dlqEntryId);
WebhookJob::dispatch(
$entry->topic,
$entry->payload,
$entry->webhook_id
);
$entry->update(['status' => 'replayed']);
}
Replay is a core part of operational maturity. Our article on webhook replay systems in Shopify covers batch replay, selective replay, and safeguards against replay loops.
Monitoring and Observability
You cannot fix what you cannot see. Retry strategies only work if you can observe them in action.
Track these metrics at minimum:
| Metric | Why it matters |
|---|---|
| Webhook success rate | Overall health signal |
| Retry count per event | Detects struggling dependencies |
| DLQ entry rate | Early warning of outages |
| Processing latency | Finds slow handlers |
| Duplicate detection count | Confirms idempotency works |
Set alerts on DLQ growth and sudden drops in success rate. A spike in retries often signals a downstream problem before customers notice. Our guide on webhook monitoring and observability explains how to build dashboards and alerting that catch issues early.
Verifying Webhooks Before You Retry
Retries only make sense for legitimate webhooks. Before you store or process anything, verify the HMAC signature Shopify sends.
A failed signature check is a non-retryable error. You should reject it with a 401 and never queue it. Retrying an invalid webhook accomplishes nothing and may indicate an attack.
public function verifyHmac(Request $request): bool
{
$hmac = $request->header('X-Shopify-Hmac-Sha256');
$calculated = base64_encode(
hash_hmac('sha256', $request->getContent(), config('shopify.secret'), true)
);
return hash_equals($hmac, $calculated);
}
Always verify before you queue. Our guide on secure webhook verification systems covers timing-safe comparison and secret rotation.
Putting It All Together: A Complete Retry Flow
Here is how all the pieces connect into one reliable pipeline.
- Shopify sends a webhook to your endpoint.
- You verify the HMAC signature. Invalid requests get rejected immediately.
- You store the raw payload and return
200fast. - A background worker picks up the job.
- The worker checks idempotency to avoid duplicate processing.
- On transient failure, it retries with exponential backoff plus jitter.
- After the retry ceiling, the job moves to a dead letter queue.
- Alerts fire, you fix the root cause, and you replay from the DLQ.
This flow is the backbone of reliable Shopify webhook consumers. Each layer handles a different class of failure, and together they make data loss nearly impossible.
Tuning Retries for Different Webhook Types
Not every webhook deserves the same retry treatment. An order creation event is critical. A theme update notification might not be.
| Webhook topic | Retry priority | Suggested max attempts |
|---|---|---|
| orders/create | Critical | 8 |
| orders/paid | Critical | 8 |
| inventory_levels/update | High | 6 |
| products/update | Medium | 4 |
| customers/update | Medium | 4 |
| app/uninstalled | Low | 2 |
Prioritizing retries lets you spend infrastructure where it matters. Critical events get aggressive retries, while low-value events fail fast. This kind of tuning connects to broader scalable Shopify integration patterns and helps you reduce Shopify API costs by avoiding wasted work.
Retries During Traffic Spikes
Flash sales and product drops stress every part of your system, including webhook processing. During these events, your downstream services slow down, timeouts increase, and retries pile up.
Backoff and jitter prevent retries from making the problem worse. You should also consider load balancing strategies to spread webhook processing across workers, and the techniques in our scaling Shopify systems during flash sales guide.
If your app serves many stores at once, retry storms can affect every tenant. The patterns in scaling Shopify apps to millions of requests show how to isolate failures so one store’s outage does not take down the rest. A solid queue infrastructure for Shopify apps is essential here.
Common Mistakes to Avoid
A few recurring errors undermine otherwise solid retry systems.
Processing synchronously inside the webhook request is the biggest one. It causes timeouts that trigger false failures. Always accept fast and process in the background.
Retrying non-retryable errors is another. A validation failure will never succeed, so retrying it just burns resources. Classify your errors.
Skipping idempotency guarantees duplicate side effects. Every retry system needs it.
Finally, ignoring your dead letter queue means failed events rot unnoticed. Always alert on DLQ entries and have a replay path ready.
Final Thoughts
Shopify webhook retry strategies are not optional for serious integrations. Shopify’s built-in retries help, but they give up after 48 hours and offer no control. Your own retry layer, built on accept-fast processing, exponential backoff, jitter, idempotency, dead letter queues, and replay, is what keeps your data consistent under real-world failures.
Start with the basics: validate fast, queue the work, and add backoff. Then layer in idempotency and a dead letter queue. Finally, add monitoring so you can see failures before your customers do. Done right, webhooks become one of the most dependable parts of your Shopify stack rather than a source of silent bugs.
Frequently Asked Questions
How long does Shopify retry a failed webhook?
Shopify retries failed webhook deliveries over a 48-hour window with increasing gaps between attempts. If every attempt fails, Shopify removes the subscription and emails you a warning.
Should I rely on Shopify’s built-in retries?
No. Shopify’s retries are a coarse safety net with no control and a hard 48-hour limit. Build your own retry layer using a queue, backoff, and a dead letter queue for full reliability.
What is exponential backoff in Shopify webhooks?
Exponential backoff increases the wait time between retry attempts after each failure. It prevents your system from overwhelming a struggling downstream service and gives dependencies time to recover.
Why do I need idempotency with webhook retries?
Retries can run the same logic twice, causing duplicate orders or emails. Idempotency uses the webhook ID to detect already-processed events and makes retries safe to repeat.
What is a dead letter queue for webhooks?
A dead letter queue stores webhooks that exhausted all retry attempts. Instead of losing the data, you park it for inspection, alerting, and later replay once the root cause is fixed.
How many times should I retry a failed webhook?
Six to eight attempts over a 24 to 48 hour window suits most critical events. Tune the ceiling based on webhook priority, retrying critical order events more aggressively than low-value notifications.
