21 Sept 2026 · 4 min read
MSI reservations and why inventory drifts
Understand MSI reservations and compensation so your stock numbers stop drifting between order, shipment and refund
Part 2 of the MSI / Inventory series — reservations are the ledger, not the stock.
The numbers drifted by three units a day
A merchant reconciled inventory every Monday and found the same pattern: salable quantity sagged a few units below physical count each week, then jumped after a manual reindex. No theft, no miscount. The cause was the reservation ledger in inventory_reservation: order placement appends a negative reservation, and shipment, cancellation, or refund must append a matching compensation. A custom one-click checkout skipped the standard placement events, and a shipment integration deducted source qty directly. Both broke the ledger. By Friday the drift was large enough to block checkout on a product sitting on the shelf.
Why this matters
Reservations let checkout sell fast without locking stock rows. Placing an order does not decrement inventory_source_item; it appends a row to inventory_reservation with a negative quantity. Salable quantity is computed as stock qty minus open reservations, so the storefront stays correct under concurrency. But every reservation must be compensated exactly once: shipment appends a positive compensation and decrements the source, cancellation appends a positive compensation, refund appends one for the credited items. Miss one and numbers drift silently for weeks because nothing errors, the ledger just never balances.
How it works
The lifecycle per order item:
- Placement appends
inventory_reservationrows (negative qty,stock_id,sku,metadatawith order increment id). Checkout validation already confirmed salable quantity covers the order. - Shipment appends a positive compensation and decrements
inventory_source_item.quantityon the selected source. Source selection already chose which source ships. - Cancellation appends a positive compensation for the unshipped qty. No source qty changes.
- Credit memo / refund appends a positive compensation and, for returns to stock, increments the source qty.
Salable quantity = stock qty minus open reservations. The stock views (inventory_stock_1, etc.) compute this live, so a stuck reservation depresses salability until compensated.
Diagnose drift with SQL before touching code:
SELECT sku, stock_id, SUM(quantity) AS open_reservations, COUNT(*) AS rows_count
FROM inventory_reservation
GROUP BY sku, stock_id
HAVING SUM(quantity) <> 0
ORDER BY ABS(SUM(quantity)) DESC
LIMIT 20;
SELECT reservation_id, stock_id, sku, quantity, metadata
FROM inventory_reservation
WHERE sku = 'SKU-123'
ORDER BY reservation_id DESC
LIMIT 20;
Magento ships the repair tooling; use it in order:
bin/magento inventory:reservation:list-inconsistencies
bin/magento inventory:reservation:create-compensations
bin/magento indexer:reindex inventory
Custom checkout or ERP flows must emit the same events (PlaceReservationsForSalesEvent, AppendReservations) or bypass MSI consistently through the inventory APIs. The worst option is a hybrid: deducting source qty directly while core also appends reservations double-counts every order. Relevant config: Stores > Configuration > Sales > Inventory Management for the reservation cleanup expectations, plus cron health for inventory_cleanup_reservations and sales_clean_orders consumers. A cron backlog delays compensation for hours even when the code is correct, so check cron_schedule for stuck consumers_runner jobs before blaming logic.
Pitfalls
- Custom order flows that deduct qty directly and double-count. If your headless checkout writes
inventory_source_iteminstead of going throughPlaceOrderservices, every order decrements twice: once by your code, once by the shipment compensation. Route all placements through the inventory reservation APIs. - Partial shipments compensated against the wrong source. Compensating stock 1 while decrementing a source linked to stock 2 leaves both stocks wrong. Always resolve the source through the order's stock, not a hardcoded default.
- Cron backlog delaying compensation for hours. Reservations placed at 10:00 with a stuck queue show as unavailable until consumers run. Monitor
queue_messagedepth and keepcron_scheduleclean before tuning anything else. - Running cleanup queries by hand. Deleting rows from
inventory_reservationinstead of appending compensations destroys the audit trail and breakslist-inconsistencies. Always compensate, never delete.
Field check on a live store
Schedule inventory:reservation:list-inconsistencies as a weekly cron report, not a one-off debug step. On a healthy store it returns empty; on a drifting store the same SKUs recur with growing negative balances. For each flagged SKU, pull the reservation history ordered by id and match every negative placement against its compensation by order increment id in the metadata. Unmatched placements cluster around one event type: shipment integrations that bypass MSI, custom cancellations, or offline refunds. The merchant above found every unmatched row came from the one-click checkout within an hour of querying. Fix the event path first, run create-compensations once for the backlog, then confirm the next weekly report is clean before closing the incident. Keep the report output itself as the audit record so the next drift investigation starts from history rather than from zero.
Closing
Treat inventory_reservation as a financial ledger: append-only, balanced by compensations, reconciled with list-inconsistencies on a schedule. The merchants who stop drifting are the ones who alert on the first unmatched row instead of reconciling quarterly. Once the ledger is trustworthy, the next layer is geographic: which source serves a pickup customer.
Next in this series:
magento2-msi-instore-pickup