<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Muhammad Masad Ashraf</title>
    <description>The latest articles on Forem by Muhammad Masad Ashraf (@masadashraf).</description>
    <link>https://forem.com/masadashraf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1849252%2F2176456a-d7d2-451a-a400-020a2702872f.jpg</url>
      <title>Forem: Muhammad Masad Ashraf</title>
      <link>https://forem.com/masadashraf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/masadashraf"/>
    <language>en</language>
    <item>
      <title>Shopify Idempotency Strategies: A Developer's Survival Guide</title>
      <dc:creator>Muhammad Masad Ashraf</dc:creator>
      <pubDate>Tue, 05 May 2026 20:11:23 +0000</pubDate>
      <link>https://forem.com/masadashraf/shopify-idempotency-strategies-a-developers-survival-guide-57k1</link>
      <guid>https://forem.com/masadashraf/shopify-idempotency-strategies-a-developers-survival-guide-57k1</guid>
      <description>&lt;p&gt;You built a Shopify integration. It works great. Until it doesn't.&lt;/p&gt;

&lt;p&gt;A customer's network drops right after the checkout POST fires. They refresh. The browser retries. Shopify receives two identical order creation requests. You now have a double order, a double charge, and a very confused customer.&lt;/p&gt;

&lt;p&gt;This is an idempotency problem. And it is more common than most Shopify developers expect.&lt;/p&gt;

&lt;p&gt;What Is Idempotency in a Shopify Context?&lt;br&gt;
An operation is idempotent if running it once or a hundred times produces the same result. In Shopify integrations, this covers:&lt;/p&gt;

&lt;p&gt;• API mutations (order creation, fulfillment, refunds)&lt;br&gt;
• Webhook processing (order.paid, inventory.update)&lt;br&gt;
• Background job retries&lt;br&gt;
• Third-party sync operations&lt;/p&gt;

&lt;p&gt;Strategy 1: Use Idempotency Keys on REST API Calls&lt;br&gt;
Shopify's REST Admin API accepts an Idempotency-Key header on POST requests. Generate a UUID v4 per logical operation and send it with the request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;v4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uuidv4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Generate ONCE per business event, store it const idempotencyKey = uuidv4();  await fetch('https://your-store.myshopify.com/admin/api/2024-01/orders.json', {   method: 'POST',   headers: {     'Content-Type': 'application/json',     'X-Shopify-Access-Token': process.env.SHOPIFY_TOKEN,     'Idempotency-Key': idempotencyKey, // same key on every retry   },   body: JSON.stringify(orderPayload) });&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key rule: Generate the key once. Store it before the request. Reuse it on every retry.&lt;/p&gt;

&lt;p&gt;Strategy 2: Deduplicate Webhooks with X-Shopify-Webhook-Id&lt;br&gt;
Shopify does not guarantee exactly-once webhook delivery. Every delivery includes an X-Shopify-Webhook-Id header. Use it as your deduplication key.&lt;/p&gt;

&lt;p&gt;Express.js webhook handler&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/orders/paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webhookId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-shopify-webhook-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check Redis for this ID (48hr TTL)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`webhook:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;webhookId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;already_processed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mark as seen BEFORE processing&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`webhook:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;webhookId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;172800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// Now safely process   await processOrderPaid(req.body);   res.status(200).json({ status: 'ok' }); });&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why store BEFORE processing? If processing fails and you stored AFTER, the next retry will be treated as a duplicate and skipped. Store the key first, then process, and let your retry logic handle actual failures separately.&lt;/p&gt;

&lt;p&gt;Strategy 3: Safe Retries with Exponential Backoff&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;retryWithBackoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxRetries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;     &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// always pass same key     } catch (err) {       if (attempt === maxRetries - 1) throw err;       const baseDelay = Math.pow(2, attempt) * 1000;       const jitter = Math.random() * 400 - 200;       await sleep(baseDelay + jitter);     }   } }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The jitter prevents thundering herd problems when many clients retry simultaneously after an outage.&lt;/p&gt;

&lt;p&gt;Strategy 4: Database-Level Unique Constraints&lt;br&gt;
App-level checks can fail under race conditions. Two requests can both pass your "does this exist?" check before either writes. Add a database constraint as the hard backstop.&lt;/p&gt;

&lt;p&gt;PostgreSQL: prevent duplicate order processing&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_idempotency&lt;/span&gt;   &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;processed_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;-- Catch the constraint violation in your app try {   await db.query(     'INSERT INTO processed_events (idempotency_key, result) VALUES ($1, $2)',     [key, JSON.stringify(result)]   ); } catch (err) {   if (err.code === '23505') { // unique violation     const existing = await db.query(       'SELECT result FROM processed_events WHERE idempotency_key = $1',       [key]     );     return JSON.parse(existing.rows[0].result); // return cached result   }   throw err; }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GraphQL: No Native Header, So Use Upserts&lt;br&gt;
Shopify's GraphQL API does not support the Idempotency-Key header. Design mutations to be naturally idempotent instead.&lt;/p&gt;

&lt;p&gt;Use productUpdate (upsert) instead of productCreate where possible&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UpdateProductInventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;inventoryAdjustQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;inventoryItemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;availableDelta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$qty&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;inventoryLevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;userErrors&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For operations with no upsert, check for existing records first and return the existing result if found.&lt;/p&gt;

&lt;p&gt;Quick Reference: What to Use Where&lt;/p&gt;

&lt;p&gt;REST mutations: Idempotency-Key header + store key before request&lt;br&gt;
GraphQL mutations: Upserts + client-side dedup table&lt;br&gt;
Webhooks: X-Shopify-Webhook-Id + Redis with 48hr TTL&lt;br&gt;
Background jobs: Job ID as idempotency key + status tracking&lt;br&gt;
Database: Unique constraint on idempotency_key as final guard&lt;/p&gt;

&lt;p&gt;Further Reading&lt;br&gt;
&lt;a href="https://kolachitech.com/shopify-webhooks/" rel="noopener noreferrer"&gt;Shopify Webhooks Deep Dive&lt;/a&gt; | &lt;a href="https://kolachitech.com/fault-tolerant-shopify-integration/" rel="noopener noreferrer"&gt;Fault-Tolerant Shopify Integration&lt;/a&gt; | &lt;a href="https://kolachitech.com/queue-based-shopify-webhook-processing/" rel="noopener noreferrer"&gt;Queue-Based Webhook Processing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What idempotency patterns do you use in your Shopify stack? &lt;br&gt;
&lt;a href="https://kolachitech.com/idempotency-strategies-in-shopify-systems/" rel="noopener noreferrer"&gt;Read more&lt;/a&gt; &lt;/p&gt;

</description>
      <category>shopify</category>
      <category>development</category>
    </item>
    <item>
      <title>Queue-Based Shopify Webhook Processing: Why It Matters and How to Build It</title>
      <dc:creator>Muhammad Masad Ashraf</dc:creator>
      <pubDate>Fri, 01 May 2026 23:39:00 +0000</pubDate>
      <link>https://forem.com/masadashraf/queue-based-shopify-webhook-processing-why-it-matters-and-how-to-build-it-5gff</link>
      <guid>https://forem.com/masadashraf/queue-based-shopify-webhook-processing-why-it-matters-and-how-to-build-it-5gff</guid>
      <description>&lt;p&gt;Shopify webhooks are HTTP POST requests fired on store events: orders, inventory updates, checkouts, customers. By default, your endpoint handles them synchronously. That works until traffic spikes.&lt;/p&gt;

&lt;p&gt;The async pattern:&lt;br&gt;
&lt;code&gt;Shopify fires webhook&lt;br&gt;
  → Receiver validates HMAC, pushes to queue, returns 200&lt;br&gt;
  → Worker pulls job, runs business logic, marks complete&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Your endpoint responds in milliseconds. Your worker takes as long as it needs.&lt;/p&gt;

&lt;p&gt;The two things that actually trip people up in production:&lt;/p&gt;

&lt;p&gt;Idempotency. Shopify retries on failure. If your worker processes a duplicate &lt;strong&gt;&lt;em&gt;orders/paid&lt;/em&gt;&lt;/strong&gt;, you might charge a customer twice. Fix: check the webhook ID in Redis before processing. Skip if seen. Simple.&lt;/p&gt;

&lt;p&gt;Dead letter queues. Jobs that fail all retries need somewhere to go. Log them, alert on them, reprocess them manually. Without a DLQ, failed jobs vanish and you find out from a merchant complaint.&lt;/p&gt;

&lt;p&gt;Stack recommendations: BullMQ + Redis for Node.js, Laravel Queues for PHP, Celery for Python, SQS + Lambda for AWS-native.&lt;/p&gt;

&lt;p&gt;Full post with code, priority queue tiers by webhook topic, and a go-live checklist: &lt;a href="https://kolachitech.com/queue-based-shopify-webhook-processing/" rel="noopener noreferrer"&gt;https://kolachitech.com/queue-based-shopify-webhook-processing/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>webhooks</category>
      <category>architecture</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
