Why the till beats the website to the last unit, reservation versus available-to-promise, how much sync latency a SKU absorbs, and reconciling by movement.
Key takeaways
- The till is the fastest writer in the estate: it closes a sale in one second while a web order passes through cart, gateway and confirmation. The last unit is therefore sold twice at the website's expense.
- "Available" is not a stored field. It is a calculation: on hand minus committed, minus reserved, minus unavailable buckets such as damaged or safety stock.
- A reservation must be created when the customer starts paying, not when payment clears. In WooCommerce the setting that produces this behaviour is Hold stock (minutes): it holds stock for an unpaid order for the number of minutes you enter, then cancels the order and releases the stock. Leave it blank to disable. No default value is documented, so check what yours is set to.
- The risk window for a SKU equals sync latency multiplied by its sales rate. An item sold once a month tolerates five minutes of lag; an item sold ten times a day does not.
- A daily reconciliation that compares only final quantities tells you a gap exists. Comparing movements tells you which receipt, return or sync caused it.
Overselling happens because the till and the website each hold their own picture of what is left, and the till writes first. Preventing it takes three things: one available-to-promise calculation, a reservation created when payment starts rather than when it clears, and a reconciliation job that compares movements instead of final quantities.
Why the till wins the race for the last unit
The till wins because an in-store sale is one atomic action while a web sale is a sequence of four steps, each taking time. Here is a worked case: SKU FR-2210-42, one unit at the Ramat Gan branch.
- 14:32:07 - the assistant scans the barcode. The record is written locally.
- 14:32:09 - a web customer clicks Pay on the same SKU. The page checked availability 40 seconds earlier.
- 14:32:11 - the gateway approves the charge. A web order now exists.
- 14:32:14 - the till transmits the sale movement to the ERP.
- 14:32:38 - the storefront pulls a stock feed and sets the quantity to 0, twenty-seven seconds late.
Two documents, one unit. Note that nothing malfunctioned. Every system did precisely what it was designed to do, and the whole failure lives between 14:32:07 and 14:32:38. Any fix that does not touch that window - a faster sync plugin, a new inventory system, a daily report - will not prevent the next occurrence.
What the word "available" actually means
Available is the output of a calculation, not a field anyone writes. The formula that holds up:
available = on hand - committed to unfulfilled orders - reserved for open carts - unavailable buckets - safety stock
Shopify's GraphQL Admin API documents eight distinct quantity states: incoming, on_hand, available, committed, reserved, damaged, safety_stock and quality_control, where on_hand is the sum of available, committed and unavailable. The documentation also states that quantities in the committed state cannot be adjusted or moved through the API, because they are derived from orders. WooCommerce works differently: an order arrives as pending, and stock is actually reduced when the order moves to processing or on-hold.
| What is counted | Till | Website | ERP or inventory system |
|---|---|---|---|
| Physical stock in branch | Source of truth at scan time | Does not know | Updated from movements |
| Committed to open orders | Usually absent | Present, by order status | Present, by order line |
| Cart reservation | Not applicable | A timer or a reservations table | Usually unknown |
| Safety stock | No | Can be modelled as a separate pool | A policy field |
| Returns not yet inspected | Back in stock immediately | Does not know | Depends on location setup |
That last row causes a disproportionate share of gaps: a return taken at the till re-enters stock at once, while on the website it does not exist until somebody approves it.
Reservation or available-to-promise - which one do you need?
Reservation is right when the unit is identified; available-to-promise is right when units are interchangeable. They are not substitutes for one another:
- Reservation holds a specific unit or quantity for a specific order, with an expiry and an owner. Correct for a frame by serial number, a vehicle, a floor-model appliance.
- Available-to-promise computes how much can be promised right now without holding a specific unit. Correct for a shirt in size M, for screws, for anything counted rather than identified.
- The deciding rule: if returning a unit to stock requires a human action, you need a reservation with an expiry. If it happens anyway when an order is cancelled, a calculation is enough.
Where serial numbers exist the choice is already made - see serial inventory in an optical store.
How much sync latency can you actually afford?
A SKU's risk window equals sync latency multiplied by its sales rate, so the answer differs per SKU. The arithmetic: an item selling 10 times a day across a 10-hour trading day sells on average once every 60 minutes, so a 5-minute sync lag creates roughly an 8% chance of a double sale while one unit remains. The same lag on an item selling once a week is noise.
The design consequence is that you do not sync everything at the same cadence:
- SKUs at one or two units move to push updates from the till, not a scheduled feed.
- SKUs with deep stock stay on periodic sync, which is far cheaper in API call quota.
- Any SKU crossing the threshold you set moves into group one automatically.
- When the storefront API throttles you, tier the items rather than raise the frequency - covered in handling 429 rate limits in store sync.
What the site shows when sync goes down
When sync fails, the default is to stop promising rather than keep selling against a stale number. That decision is made in advance, in writing, per product group:
- Show "out of stock" for SKUs below your threshold instead of a quantity nobody can vouch for.
- Allow an order with an extended lead time and a staff check, instead of promising next-day delivery.
- Block serialised items outright, because a duplicate sale of an identified unit cannot be compensated easily.
- Record the outage window, so that day's reconciliation knows which interval to examine.
- Alert a person, not only a log. A sync that fails silently is the single largest source of drift.
In Shopify, whether a variant keeps selling at zero is controlled by inventoryPolicy at the variant level, with exactly two values: DENY and CONTINUE. That is a commercial decision recorded in code, not a default worth inheriting.
The reconciliation job that finds the cause, not just the gap
A reconciliation comparing final quantities tells you two units are missing and nothing else. A reconciliation comparing movements tells you exactly which movement was dropped. So every stock movement is stored as a record, not as an update to a quantity field:
| Field | Example |
|---|---|
movement_id | MV-2026-0912-004417 |
sku | FR-2210-42 |
type | sale / return / receipt / transfer / adjustment / reservation |
source | pos-ramat-gan-2 |
qty_delta | -1 |
occurred_at | 2026-09-12T14:32:07+03:00 |
synced_at | 2026-09-12T14:32:38+03:00 |
ref_doc | POS-88213 |
With that structure the nightly job runs one query: the sum of qty_delta per SKU against the quantity each system reports. A gap is presented together with the movements that produced it, and the distance between occurred_at and synced_at immediately says whether the cause was slow sync or a movement that never arrived at all. The wider version of this layer against a central system is covered in connecting an online store to an ERP.
Sources
Frequently asked questions
Does an inventory sync plugin solve this?
A plugin shortens the lag but does not remove it, so it narrows the risk window without closing it. The items sold twice are the ones down to a single unit, which is exactly the state where every second of delay counts. A plugin with no reservation and no movement-level reconciliation postpones the problem.
What do you do when the website sold something that no longer exists?
Treat it as a defined process rather than an improvisation: flag the order line as an exception, create a task to contact the customer inside a pre-agreed window, and offer a substitute or a cancellation. In parallel, write an adjustment movement with a reason code so the monthly reconciliation knows the gap was explained.
Does safety stock prevent overselling?
Safety stock lowers the frequency but is not a prevention mechanism. It simply hides part of the stock from the calculation, so the race between till and website happens over the third unit rather than the last one. It is useful when latency is known and stable, and harmful when used to avoid fixing the sync.
What is the difference between committed and reserved stock?
Committed stock belongs to an order that exists and has not shipped. Reserved stock is temporarily removed from available for a process that has not yet become an order, such as a cart in payment or a quote. The practical difference: a reservation expires on its own clock, while committed stock is released only by fulfilment or cancellation.
Keep reading
Related service
Inventory & Purchasing
SKU-level stock, reorder rules and an approval flow that leaves a record.
About the author
Yehonatan Saadia
Freelance automation, web & MVP developer
I'm Yehonatan Saadia, a senior developer who builds business automation, custom websites, and MVPs for small and mid-sized companies across the US, Europe, and Israel. These guides come from real client work, not theory.
Work with meHave a project like this?
Tell me what you're trying to automate or build. I reply within 24 business hours with a few targeted questions, then we walk through it on a free 30-minute call, with no commitment. You come away with a scope, a timeline and a fixed price - or a straight answer that it isn't worth building.
