Inventory Sync Hell: Why Your Shopify Store Oversells on Black Friday (And the Architecture That Fixes It)
Overselling on peak days is rarely a Shopify bug. It's a sync architecture problem. Here's how we redesigned a multi-channel inventory pipeline to stop the bleed without rewriting the ERP.

Every November we get the same call. A merchant did record revenue on Friday, and by Monday morning their support inbox is on fire because half the orders can't be fulfilled. The product page said "in stock". The warehouse says otherwise.
Overselling is almost never a Shopify bug. It's a synchronization architecture problem, and it gets exposed the moment traffic spikes. This is the breakdown we wish more teams had before peak season hit.
Why Overselling Happens Even When "Everything Is Connected"
The naive mental model goes like this: warehouse system has the truth, Shopify mirrors it, customers buy, stock decrements. In reality, a mid-market merchant usually has four or five systems all claiming to know the inventory:
- The ERP or WMS (NetSuite, SAP B1, Odoo, a custom Postgres table)
- Shopify itself, with its own
inventoryLevelper location - A marketplace connector (Amazon, Mercado Libre, eBay)
- A POS for physical stores
- A 3PL portal that sometimes disagrees with all of the above
Each of these has its own write cadence. The ERP might push a full snapshot every 15 minutes. The marketplace connector pulls on a 5-minute cron. Shopify decrements in real time at checkout. The POS batches at end of day. On a calm Tuesday, the drift is invisible. On Black Friday, the drift becomes the story.
The Three Failure Modes We See Most
Snapshot overwrite races. ERP pushes a full inventory snapshot to Shopify. Between the moment it read the warehouse and the moment Shopify applied it, 47 orders came in. Those decrements get clobbered.
Webhook backpressure. Shopify fires orders/create webhooks to your middleware. Your middleware is also receiving traffic from marketplaces, the POS, and the ERP. Webhooks queue up. By the time the ERP learns about an order, the same SKU has been sold three more times.
Optimistic display. The PDP shows "in stock" based on a cached value that's 90 seconds stale. At checkout, Shopify's real inventory check passes too, because the truth hasn't propagated yet. The order is accepted. The warehouse has nothing on the shelf.
Pick a Source of Truth (And Actually Mean It)
The first architectural decision is also the one most teams refuse to make: which system owns the number?
In our experience, the answer should almost always be the WMS or 3PL, not the ERP and definitely not Shopify. The warehouse is where physical reality lives. Everything else is a projection.
Once you've picked it, every other system becomes read-only for that field. The ERP can suggest a reorder point. Shopify can display a count. The marketplace can show availability. None of them are allowed to write back to the source of truth without going through a single reconciliation path.
This sounds obvious. It is routinely violated by Shopify apps that "helpfully" let merchants edit stock from the admin UI, by ERP integrations that push snapshots without checking deltas, and by POS systems that decrement locally and sync later.
Event-Driven Sync, Not Snapshot Sync
The pattern we land on for most mid-market clients looks like this:
- WMS emits an event whenever physical stock changes (receipt, pick, cycle count, return).
- A reconciliation service consumes those events and computes the available-to-sell number per channel.
- The service writes to Shopify, marketplaces, and the ERP via their respective APIs, with idempotency keys.
- Shopify's own
orders/createandrefunds/createwebhooks feed back into the same service so it can adjust the projection between WMS events.
The key word is projection. The reconciliation service is not the source of truth. It's a materialized view that combines confirmed physical stock with in-flight orders that haven't been picked yet.
A Minimal Reconciliation Loop
Here's the shape of the consumer, in pseudo-Node, stripped to the essentials:
async function handleEvent(event: InventoryEvent) {
const lock = await redis.acquireLock(`sku:${event.sku}`, { ttl: 5000 });
try {
const physical = await wms.getOnHand(event.sku);
const reserved = await db.getReservedQty(event.sku); // unpicked orders
const buffer = await config.getSafetyBuffer(event.sku); // per-channel
const available = Math.max(0, physical - reserved - buffer);
await shopify.inventoryLevels.set({
sku: event.sku,
locationId: SHOPIFY_LOCATION_ID,
available,
idempotencyKey: `${event.id}:shopify`,
});
await marketplace.updateStock(event.sku, available, {
idempotencyKey: `${event.id}:mkt`,
});
await db.recordSync(event.id, available);
} finally {
await lock.release();
}
}
Three things matter here:
- The per-SKU lock. Two events for the same SKU cannot reconcile concurrently. Without this, you get write races against Shopify's inventory API.
- The safety buffer. Per channel, per SKU. Shopify might get the full available number. A marketplace with slower webhook propagation might get available minus 2. It's not elegant. It works.
- Idempotency keys. Both your own DB and most modern commerce APIs accept them. Use them. Retries are not optional at peak load.
The Shopify-Specific Gotchas
Shopify's inventory API has quirks worth calling out before you design around them.
inventoryLevels/set is absolute. It overwrites. If you compute available = 12 based on a stale read and write it, you've just undone any decrements that happened in the last few hundred milliseconds. Prefer inventoryLevels/adjust (delta-based) when you can, and reserve set for reconciliation passes you've explicitly locked.
Shopify also rate-limits aggressively per shop, not per app. If your reconciliation service and a third-party app are both hammering the inventory endpoint, you'll get 429s and your retries will compound. Centralize all writes through one queue with a token bucket sized below the published limit.
Finally, multi-location stores need explicit per-location logic. Shopify's checkout will happily sell from a location that has stock even if the customer expected it from the nearest one. If you're running ship-from-store, this matters for delivery promises.
What About Headless?
Headless setups (Hydrogen, Next.js with the Storefront API, custom React frontends) make the display layer faster but also make caching more aggressive. A PDP rendered at the edge with a 60-second cache will show stale stock to thousands of users during a flash sale.
The fix isn't to disable caching. It's to split the cache:
- Product metadata (title, description, images, price): long cache, invalidated on product update webhooks.
- Inventory state: short cache or a separate fetch from a low-latency endpoint that reads your reconciliation service directly.
We've seen teams write a tiny /api/stock?sku=... endpoint that bypasses the storefront API entirely and reads from Redis. PDP shell loads from the edge cache. Stock badge hydrates in under 50ms from the dedicated endpoint. Best of both worlds.
If you want a deeper look at the tradeoffs, our headless commerce engineering services page goes into the patterns we use per stack.
Reconciliation Beats Prevention
Here's the hard truth: at sufficient scale, you will oversell occasionally. The goal is not zero overselling. The goal is fast detection and graceful recovery.
Build a reconciliation job that runs every 5 to 15 minutes during peak and compares Shopify's available count against your projection. Alert on any drift over a configurable threshold. Auto-correct when it's safe; page a human when it isn't.
Also build the customer-facing recovery flow before you need it. A pre-written email template, a refund-plus-discount-code automation, and a clear escalation path for high-value orders. The merchants who handle oversells well don't avoid them entirely. They apologize fast, refund faster, and offer something that turns the incident into retention.
Where We'd Start
If you're staring down peak season and this sounds uncomfortably familiar, do three things this week:
- Draw the actual data flow. Every system, every webhook, every cron. Most teams discover the bug just by drawing it.
- Pick one source of truth and make every other system read-only against that field. Politically painful. Worth it.
- Add a reconciliation job that compares Shopify's inventory to your warehouse number every 10 minutes and logs the drift. You don't have to fix it yet. You just have to see it.
The architecture comes after the visibility. Teams that skip step three end up rebuilding the same broken pipeline with fancier tools.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Variant Switching Without a Full Page Reload: A Practical PDP Pattern for Shopify
Most Shopify themes still reload the PDP when a customer picks a size. Here's the pattern we use to swap variants in under 150ms without breaking SEO, analytics, or the cart.

WhatsApp Checkout in LATAM: What We Learned Wiring a Shopify Store to a Conversational Funnel
We replaced a third of a Shopify store's checkout volume with a WhatsApp-driven flow. Here's the architecture, the conversion math, and the parts we'd build differently next time.

Headless Shopify Is Not Free: A Honest Look at Hydrogen vs Liquid for Mid-Market Stores
We've shipped both Hydrogen storefronts and heavily customized Liquid themes for stores doing $5M–$50M GMV. Here's where headless actually pays for itself, and where it quietly bleeds money for two years before anyone admits it.
