← Back to blog

Magento 2 MSI Explained: Sources, Stocks and Sales Channels

Rodrigo Biazzi

Multi-Source Inventory, usually called MSI, has been the inventory system in Magento since 2.3. Many stores still run it in single source mode without ever looking at it, and then get surprised by salable quantity, reservations or stock that does not match the ERP. This article explains the model behind MSI in Magento Open Source and Adobe Commerce 2.4.8 and how to work with it from code.

The three building blocks

Sources

A source is a physical place that holds stock: a warehouse, a store, a drop shipper. Each source has a code, an address and an enabled flag. The quantity of a product at a source is a source item, identified by SKU and source code, with a quantity and a status of in stock or out of stock.

Every installation has a source called default. It cannot be deleted. When MSI runs in single source mode, everything lives in that one source and the admin product form shows the classic Quantity field.

Stocks

A stock is a virtual group of sources. It answers the question "how much of this SKU can this sales channel sell?". A stock is linked to one or more sources, in priority order, and the quantity it offers is the sum of the in stock source items of the linked, enabled sources.

The Default Stock, with ID 1, also cannot be deleted. It is linked to the default source, and by default it serves the main website.

Sales channels

A sales channel connects a stock to the place where products are sold. In the core the only channel type is the website. Each website is assigned to exactly one stock, and a stock can serve several websites. The storefront and the checkout of a website check salability against that stock.

So the chain is website, then stock, then sources, then source items. Stock is not chosen per store view or per customer.

When multiple sources make sense

MSI is useful when you actually ship from more than one place, or when different websites sell from different warehouses. Typical cases are a country site that ships from a local warehouse, physical stores that also fulfill online orders, or a drop shipper whose quantity you receive by feed.

If you have one warehouse and one website, single source mode is the right choice. Additional sources add complexity in the admin, in imports and in every integration, without a benefit.

Source selection and deduction

Placing an order does not deduct quantity from any source. It creates a reservation against the stock, which lowers the salable quantity. The source is chosen later, when the order is shipped.

When you create a shipment in a multi source setup, the Source Selection Algorithm suggests which sources to ship from. The core ships with a priority based algorithm, which uses the order of sources in the stock, and a distance based algorithm, which can use Google Maps or offline geocoding data you import. You can also pick sources by hand on the shipment page.

When the shipment is saved, the quantity is deducted from the chosen source items and a compensating reservation is added, so the salable quantity stays correct. The whole reservation mechanism, and how to fix it when it drifts, is in salable quantity and reservations.

Tables behind MSI

TableContent
inventory_sourceSources and their addresses
inventory_stockStocks
inventory_source_stock_linkWhich sources belong to which stock, with priority
inventory_stock_sales_channelWhich website uses which stock
inventory_source_itemQuantity and status per SKU and source
inventory_reservationReservations per SKU and stock
inventory_stock_NIndexed quantity and salability per stock

For the default stock, inventory_stock_1 is a database view over the legacy cataloginventory_stock_status table. Additional stocks get real index tables built by the inventory indexer. This is why the legacy cataloginventory_stock_item table is still kept in sync for the default source, and why code that writes only to the legacy tables appears to work on single source stores and breaks on multi source ones.

Updating source quantities in code

Integrations should write source items through the service contracts, not through the legacy stock item model and never with direct SQL. The service updates the legacy tables for the default source and triggers the right reindexing.

<?php
declare(strict_types=1);

namespace Vendor\StockSync\Model;

use Magento\InventoryApi\Api\Data\SourceItemInterface;
use Magento\InventoryApi\Api\Data\SourceItemInterfaceFactory;
use Magento\InventoryApi\Api\SourceItemsSaveInterface;

class UpdateSourceQty
{
    public function __construct(
        private readonly SourceItemInterfaceFactory $sourceItemFactory,
        private readonly SourceItemsSaveInterface $sourceItemsSave
    ) {
    }

    /**
     * @param array<int, array{sku: string, source: string, qty: float}> $rows
     */
    public function execute(array $rows): void
    {
        $items = [];
        foreach ($rows as $row) {
            $item = $this->sourceItemFactory->create();
            $item->setSku($row['sku']);
            $item->setSourceCode($row['source']);
            $item->setQuantity($row['qty']);
            $item->setStatus(
                $row['qty'] > 0
                    ? SourceItemInterface::STATUS_IN_STOCK
                    : SourceItemInterface::STATUS_OUT_OF_STOCK
            );
            $items[] = $item;
        }

        $this->sourceItemsSave->execute($items);
    }
}

Send items in batches of a few hundred rather than one call per SKU. The save handles a list, and batching keeps the number of reindex triggers down.

The same operation is available over REST, which is the usual route for an ERP:

curl -X POST https://example.test/rest/V1/inventory/source-items \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"sourceItems":[{"sku":"ABC-001","source_code":"warehouse_sp","quantity":25,"status":1}]}'

For bulk updates from a file, the import has a Stock Sources entity with the columns source_code, sku, status and quantity.

Reading salable quantity in code

Do not read the quantity from the source item when you want to know what can be sold. Resolve the stock for the website and ask for salable quantity, which includes reservations and the out of stock threshold.

<?php
declare(strict_types=1);

namespace Vendor\StockSync\Model;

use Magento\InventorySalesApi\Api\Data\SalesChannelInterface;
use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface;
use Magento\InventorySalesApi\Api\StockResolverInterface;

class SalableQty
{
    public function __construct(
        private readonly StockResolverInterface $stockResolver,
        private readonly GetProductSalableQtyInterface $getProductSalableQty
    ) {
    }

    public function forWebsite(string $sku, string $websiteCode): float
    {
        $stock = $this->stockResolver->execute(SalesChannelInterface::TYPE_WEBSITE, $websiteCode);

        return $this->getProductSalableQty->execute($sku, (int)$stock->getStockId());
    }
}

This throws an exception for product types that do not have their own quantity, such as configurable and bundle parents, so call it on the simple children.

Common mistakes

  • Assigning a new source but not linking it to a stock. The quantity exists but no website can sell it.
  • Leaving the default source assigned with zero quantity after moving to new sources. It is harmless for salability but confusing in the admin and in imports. Unassign it from products you no longer ship from there.
  • Writing stock with SQL. It skips the legacy sync and the indexers. The storefront shows something different from the admin until a full reindex.
  • Not running message queue consumers. Several inventory operations, such as reservation updates after mass actions, run through consumers. If cron does not start them, things drift. See message queue consumers.

Can I disable MSI?

Some single source stores disable the Magento_Inventory* modules to save overhead, which falls back to the legacy CatalogInventory behavior. It works, but it has to be done for the whole group of modules and their dependents, it affects third party modules that expect MSI, and it needs to be checked again on every upgrade. I only do it when there is a measured reason. If you do it, use bin/magento module:disable with the full list and run setup:upgrade, as described in removing a module safely.

FAQ

What is the difference between a source and a stock in Magento 2?

A source is a physical location that holds quantity. A stock is a virtual group of sources that a website sells from. Websites are assigned to stocks, never directly to sources.

Can a website use more than one stock?

No. Each website is assigned to exactly one stock. A stock can be shared by several websites and can include several sources.

Why is quantity not deducted when an order is placed?

MSI creates a reservation at order placement and deducts from the source only when the order ships. Salable quantity already accounts for the reservation, so overselling is prevented.

Should I update stock through cataloginventory_stock_item?

No. Use SourceItemsSaveInterface or the inventory REST endpoints. The legacy stock item is kept in sync for the default source only and writing it directly bypasses MSI.

I am available for freelance Magento and Adobe Commerce work, including MSI setup and ERP stock integrations. If you need help with this, get in touch.

Related articles