20 Sept 2026 · 4 min read
How MSI sources, stocks and salability actually resolve
Learn how Magento MSI resolves sources, stocks and salable quantity so checkout never sells what you do not have
Part 1 of the MSI / Inventory series — the mental model behind sources, stocks and salability.
The storefront promised what the warehouse could not ship
A merchant with three warehouses called us after a promotion oversold a bestseller by 400 units. Admin showed stock. Checkout allowed the orders. The default warehouse had 50 units; the other two had the rest, reserved for regional wholesale. The problem was not quantity. It was the model: a single "qty" field was treated as the truth while Multi-Source Inventory resolves salability per stock, per website, after reservations. This post builds the correct mental model so checkout never sells what you cannot ship.
Why this matters
Multi-Source Inventory decides what "in stock" means before checkout ever runs. On a single-warehouse store the legacy cataloginventory_stock_item qty is close enough. On a multi-warehouse store it is dangerously wrong: the storefront can promise units sitting in a source that does not serve the customer's website, or units already reserved by open orders. Get the model wrong and every downstream fix (reservations, pickup, backorders) compensates for a misunderstanding instead of a real constraint.
How it works
Three concepts, in strict order: sources, stocks, salability.
Sources are physical locations. Each row in inventory_source is a place that holds units: a warehouse, a retail store, a 3PL dock. A source has a code, a country, and flags for pickup and shipment eligibility. Source items live in inventory_source_item with per-SKU quantity and status. This is the only place physical count exists.
Stocks group sources into sellable pools. Each row in inventory_stock is a logical pool, and inventory_source_stock_link maps sources to stocks with a priority order. A website maps to exactly one stock via inventory_stock_sales_channel (type website). Checkout for that website reads salability from that stock only. Two websites can share one stock or use separate stocks; that single mapping decides which warehouses can fulfill which storefront.
Salable quantity subtracts reservations. The aggregated view inventory_stock_1 (one view per stock, e.g. inventory_stock_2, inventory_stock_3) sums source quantities per SKU and subtracts open reservations from inventory_reservation. The storefront, GraphQL salable_qty, and checkout validation read this view, never raw qty. Formula: salable = sum(source qty in stock) - sum(open reservation qty). If reservations are stale, salability lies even when source qty is correct.
Source selection runs per order at shipment time, not per cart line at checkout. The Source Selection Algorithm (priority, distance, or custom) picks which sources in the stock fulfill the order. Useful commands while debugging:
bin/magento inventory:reservation:list-inconsistencies
bin/magento inventory-geonames:import --help
bin/magento indexer:reindex inventory
And the fastest ground truth check is SQL, not Admin:
SELECT sku, quantity, status
FROM inventory_source_item
WHERE source_code IN ('warehouse_east','warehouse_west') AND sku = 'SKU-123';
SELECT sku, quantity FROM inventory_stock_1 WHERE sku = 'SKU-123';
SELECT stock_id, type, code FROM inventory_stock_sales_channel;
Configuration that matters: Stores > Configuration > Inventory > Manage Stocks (stock-to-website assignment) and Manage Sources (per-source shipment and pickup flags). Assigning a source to a stock without checking the sales-channel mapping is the most common silent misconfiguration.
Pitfalls
- Treating the default source as the whole truth. On MSI the
defaultsource is one location among many. Reading its qty in custom code or ERP sync ignores every other source in the stock and every reservation. Always read the stock view or theGetSalableQuantityDataBySkuAPI. - Assigning all sources to one stock and losing per-region control. One global stock means any website can drain any warehouse. Split stocks by region or channel when fulfillment regions differ, and map each website to the right stock.
- Reading
qtyinstead of salable quantity in custom code.cataloginventory_stock_item.qtyis legacy. Custom checkout buttons, PWA stock badges, and ERP feeds must use salable quantity or they will oversell during any order spike. - Forgetting the stock view after direct SQL edits. Bulk imports that write
inventory_source_itemdirectly needbin/magento indexer:reindex inventorybefore the storefront reflects the change.
Field check on a live store
Pick one oversold SKU and walk the chain top down. First confirm the website-to-stock mapping in inventory_stock_sales_channel, then list every source linked to that stock with its priority. Compare the summed source quantities against the stock view for the SKU; a mismatch points at stale index or open reservations, not missing units. Then run inventory:reservation:list-inconsistencies and check whether the gap equals the open reservation total. On the merchant above, the pooled stock view was correct all along: the default source held 50 units while the stock held 400, and custom code reading the default source manufactured the shortage. Fifteen minutes of SQL would have prevented a week of manual reconciliation and a promotion fulfilled late.
Closing
Sources hold units, stocks pool them per website, salability subtracts what is already promised. Verify with the stock view and the sales-channel mapping before trusting any number Admin shows. With this model fixed, the next failure mode becomes visible: the reservation ledger itself.
Next in this series:
magento2-msi-reservations