Why Your Shopify 'On Hand' Inventory Never Matches What's Available to Sell
On hand is everything you have at a location; available is what's left after subtracting anything committed to an order or held back for another reason — and the two numbers almost never match. The blog posts that explain this stop at a three-way split. They miss the four unavailable states Shopify's API has tracked separately since 2023, the CSV column split that quietly broke import scripts in August 2025, and the fact that the admin's own product screen still only shows you the rolled-up total.
Shopify's "on hand" number is every unit you have at a location, full stop — including stock already promised to an unfulfilled order, held on a quality check, or set aside as safety stock. "Available" is only what's left after all of that is subtracted, which is why On Hand reading 40 while your storefront says 12 left in stock isn't a bug. It's the two fields doing exactly what they're defined to do.
Most explanations of this stop at the three-way split — committed, unavailable, available — because that's as far as the admin's own product screen goes. It's also where four states Shopify's API has tracked separately for years quietly disappear into a single number nobody breaks down for you unless you go looking.
The formula, and what it's hiding
Shopify's own documentation states it plainly: On Hand = Committed + Unavailable + Available. Committed is stock tied to something specific — an unfulfilled order, a reservation on a draft order, or a transfer marked ready to ship. Unavailable is stock set aside for any other reason. Available is whatever's left, and it's the only one of the three a customer's "add to cart" button actually checks.
A fourth number, Incoming, sits outside that formula entirely — stock on its way from a transfer or an app-managed reorder, which converts to Available automatically the moment it's marked received. It's easy to assume Incoming is already counted somewhere in On Hand. It isn't, until it physically arrives.
Say a product shows 50 on hand. If 20 units sit in unfulfilled orders and 5 are marked unavailable, available for a new sale is 25 — not 50, and not 45. Every guide that gets this far gets it right. The problem is what "unavailable" is standing in for.
What "unavailable" is actually hiding
Since a 2023 update to the Admin GraphQL API, Shopify has supported four distinct unavailable states — reserved, damaged, quality_control, and safety_stock — each one adjustable and queryable on its own. Shopify's own changelog is explicit about what a merchant actually sees: inventory sitting in any of those four states "display[s] as Unavailable to merchants that are tracking inventory in the Shopify admin." The product list, the storefront, and the plain quantity total on a product's inventory card all net these four into one figure. Whether 15 unavailable units are damaged stock waiting to be written off or safety stock deliberately held in reserve is not visible from a glance at that number — you have to open Adjust quantity on that specific inventory item and read the per-state breakdown, which is a click almost nobody scanning a product list takes.
The inventoryLevel query is where the full picture actually lives — eight named states in one response, not the admin's three or four:
query {
inventoryLevel(id: "gid://shopify/InventoryLevel/69407473686") {
quantities(names: ["available", "on_hand", "committed", "incoming", "reserved", "damaged", "quality_control", "safety_stock"]) {
name
quantity
}
}
}
If you're running any inventory sync, reorder tool, or reconciliation script against Shopify, this is the query that tells you the truth. A script reading only available and on_hand — which is what most integration guides show — will report a mismatch it can't explain, because the missing units are sitting in one of the other four states it never asked about.
Moving stock between states isn't a free edit
Those states aren't independent counters you can adjust in isolation. The inventoryMoveQuantities mutation moves a quantity from one named state to another — available into reserved, damaged, safety stock, or quality control, or between those unavailable states directly — and every state is mutually exclusive with the others, meaning a unit can only ever sit in one of them at a time. You can't accidentally double-count five units as both damaged and available; the API's write path enforces that even if nothing in the admin UI ever shows you why. That matters for anyone building automation on top of this: an app that marks stock "damaged" after a return has to move it out of wherever it currently sits, not just increment a separate field, or your on-hand total silently drifts out of sync with reality.
The CSV split that broke integrations in August 2025
In August 2025, Shopify restructured the inventory CSV for exactly this reason. The existing "On hand" column was renamed On hand (current), and a new On hand (new) column was added specifically to be edited — leaving it blank keeps live inventory untouched, while filling it in updates the count. Before this change, a common and quietly dangerous workflow was exporting the CSV to edit bin locations or other product details, then re-importing it days later without noticing the export had captured a now-stale inventory snapshot, overwriting real-time stock with numbers that were already wrong by the time the file was saved. Shopify's own changelog is direct about the consequence for anyone automating this: any script, integration, or scheduled import touching the inventory CSV built before this change needed updating, because the old single "On hand" column no longer behaves the way that automation assumed. If you're maintaining custom purchase-order or reorder tooling that reads or writes this CSV rather than the GraphQL API directly, this is worth checking today, not after a bulk import zeroes out a warehouse's real stock.
Why merchants still don't trust the term "On Hand"
Some of the distrust here predates any of this. Shopify renamed its default inventory quantity display to "On Hand" back in 2023, and the community thread merchants still post in about it runs to dozens of replies — people who built years of muscle memory around a single "inventory" number now had to relearn that "on hand" and "the number I can actually sell" were never the same thing. That's not a criticism of the current system; the underlying states are more precise than a single quantity ever was. But it's the reason so many merchants read "On Hand: 40" and assume overselling protection they don't actually have, and it's worth knowing that reaction has a specific, sourced history rather than being a one-off complaint.
How to actually check where your numbers went
For a one-off check, open the product, click into the quantity shown for the location in question under the Inventory section, and choose Adjust quantity — that panel is where the individual states, not just the rolled-up Unavailable total, actually show up. For a store-wide check, Products supports filtering by inventory quantity directly: add an Inventory filter, set it to whatever range you're chasing, save the view, and Shopify keeps it updated automatically instead of making you rebuild the filter every time. Neither of these replaces a script for anyone managing this across dozens of SKUs or multiple locations — that's what the inventoryLevel query above is for — but for confirming a single confusing number, they're faster than writing one.
Where this actually costs money
This gets expensive specifically at scale. If you're running more than one stocking location, the same concurrent-write problems that cause inventory race conditions elsewhere apply directly to state transitions — two processes moving units into and out of "reserved" for the same inventory item at the same time is a race condition regardless of whether either process ever touches the word "available." And if a product's available count sits at zero for reasons a state-by-state check would explain — stock quietly wedged in a state nobody's checked in months, or damaged stock never written off — but the product is still Active, it stays crawled, indexed, and linked from your collections and sitemap the entire time. That's the same dead-inventory pattern our own free store scanner flags across the stores it checks, and it's worth a two-minute run if you suspect your catalog has more of this than the admin's summary numbers are showing you.
Who can skip this
If you run a single location, don't sell through POS or wholesale channels that reserve stock differently, and have never had a return, a damaged shipment, or a quality hold to track, your Unavailable bucket is probably always zero and none of the four hidden states are doing anything for you. Check the basic three-way split when a number looks wrong and move on. This matters once you're running multiple locations, integrating an app that writes inventory adjustments on your behalf, or building anything — a sync tool, a reorder script, a reconciliation job — that reads Shopify's inventory data and needs to know why a total changed, not just that it did. If that's where you are and you want the reconciliation logic checked against your actual location and app setup rather than built from a guide, this is the kind of Shopify inventory and API work we do directly — get in touch and we'll tell you plainly whether the gap is in your process or in a state Shopify's admin was never going to show you.
Sources reviewed
- Shopify Help Center: Understanding inventory states
- Shopify developer changelog: New inventory states — Safety Stock, Damaged and Quality Control
- Shopify Admin GraphQL API: inventoryLevel query reference
- Shopify changelog: Prevent accidental inventory overwrites with improved CSV workflow
- Shopify Help Center: Adjusting inventory quantities
- Shopify Community: Anyone else frustrated with new "update" re: Inventory vs. On Hand Quantity?