Build1 publisher3 min readPublished
Magento's inventory_reservation is a housekeeping bill that arrives as a hosting bill
A dev.to write-up argues Multi-Source Inventory leaves an unbounded reservation table in the read path of every cart operation. The reported cost: 200-800ms per product, per add-to-cart.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- Multi-Source Inventory (MSI) has been the default in Magento since version 2.4.
- The inventory_reservation table grows without bound and every single cart operation hits it.
- The reservation flow is: add to cart triggers placeReservation, which writes a negative reservation record; placing the order links the reservation to the order; shipping the order decrements inventory_source_item and the reservation should be compensated by a positive record that cancels the original negative one.
- In theory reservations are transient, existing only to bridge the gap between cart and shipment; in practice they accumulate forever.
- Causes of accumulation listed: cancelled orders leave orphaned negative reservations; orders that fail during checkout leave reservations that are never compensated; partial shipments create partial compensation records; quote conversions that error mid-process leave dangling reservations; re-indexing, re-stocking and admin edits can create duplicate records.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A write-up published on dev.to by MageVanta describes a Magento 2 failure mode that presents as a hosting problem and is actually a retention problem: with Multi-Source Inventory enabled by default since Magento 2.4, the `inventory_reservation` table grows without bound while every cart operation reads it [1] [2]. The consequence for operators is that checkout latency degrades on stores whose traffic has not changed, so scaling the database buys time rather than a fix.
The mechanics are ordinary. MSI does not decrement stock when a customer adds to cart; `placeReservation` writes a negative reservation row, order placement links it to the order, and shipment decrements `inventory_source_item` and is supposed to write a compensating positive row that cancels the negative one [3]. In theory the rows are transient [4]. In practice, according to the author, cancelled orders leave orphaned negatives, checkouts that fail mid-flight leave reservations that are never compensated, partial shipments produce partial compensation, errored quote conversions leave danglers, and reindexing, restocking and admin edits can duplicate records [5].
The author reports that after six to twelve months of moderate traffic the table routinely reaches several million rows, and says he has seen tables above 10 million rows on stores doing 200 orders a day [6] [7]. His example store, running eight months, had 4,872,341 rows, of which 4,710,882 were older than 30 days, or 96.7 percent [8] [9]. That leaves 161,459 rows inside the window a 30-day retention policy would keep [10], and implies an accumulation rate of roughly 20,000 rows a day [11].
The cost sits in the read path. Every `addToCart` and `placeOrder` executes a `SELECT SUM(quantity) ... WHERE sku IN (...) GROUP BY sku` [12]. The default schema has a primary key on `reservation_id` and no index useful for that pattern, so the query degrades to a full table scan [13]. The author puts that at 200-800ms per cart operation per product, and 2-4 seconds added to a five-item cart [14] [15] - a range that implies 400-800ms per line item rather than the low end of his own estimate [16]. Past the point where the table no longer fits the InnoDB buffer pool, he reports disk I/O, lock contention and lock timeouts surfacing as 502s or failed checkouts [17]. That is the tell: the symptom is a web-tier error, the cause is a table nobody empties.
Measurement first: `information_schema.tables` for size, and the `performance_schema` statement digest summary to confirm the query is actually hot [18]. On a typical affected store the author says the SUM query appears in the top five slowest digests with 300-600ms averages [19]. The proposed index is a composite on `(sku, created_at)`, which he reports takes a 600ms query under 20ms on a 5 million row table by using a covering index scan [20] - about a thirtyfold reduction [21] - applied with `pt-online-schema-change` or MySQL 8 instant DDL to avoid downtime [22]. The index is the cheap half. The other half is retention: rows older than the order lifecycle, typically 30-90 days, can be compensated and archived, and the author's cron example uses a 30-day retention constant and only processes SKUs whose orders are complete or cancelled [23].
Caveats worth noting. Every latency number here is one practitioner's field observation, not a published benchmark [14] [19] [20], and the cleanup class in the post is shown only partially, so the deletion logic is not verifiable from the article as published [24]. Watch your own digest table before accepting the ranges, and watch what your row count looks like after netting compensated pairs, because deleting a negative reservation that was never actually compensated moves salable quantity.