BuildNot yet confirmed elsewhere1 publisher2 min readPublished
Shopify reserves checkout stock in MySQL by locking one row per unit
Shopify moved stock reservation from Redis into its MySQL ledger, using one row per available unit, capped at 1,000 per item and location. A dev.to analysis says the limit that remained was how long checkout held database connections, even with fast reservation queries.
The Engineer · Build desk

What happened
- Under the old Redis design, a paid order updated MySQL and cleaned up Redis in separate writes, so an interrupted sequence could oversell stock or leave it unsellable.
- Shopify's earlier attempt at MySQL reservations used a single quantity row, so competing checkouts queued on its lock and extra workers only added waiters.
- Reserve places a temporary hold when a customer starts payment, and claim permanently deducts the stock from the ledger only after payment succeeds.
- The old counter also lumped all stock together, ignoring whether a unit's location could actually fulfil the order.
Why it matters
- constraint A sold-out signal has to come from the ledger or application logic, because an empty SKIP LOCKED read only means no eligible row was free at that instant.
- exposure Shoppers competing for the last units get no documented first-come ordering, so which checkout wins depends on which rows happen to be free when its query runs.
- decision Teams copying the pattern on statement-based MySQL replication must either change replication format or run statements the manual flags as unsafe.
A dev.to analysis of Shopify's engineering account describes the fix for the hot row as a change in what gets locked. Each available unit gets its own row in an eligible pool, and a reservation takes as many rows as the quantity it needs [9]. Two checkouts for the same item can then lock separate units instead of contending for one counter [9]. Selection stays inside the eligible item and location group. It cannot borrow a row from an ineligible group [8]. The ledger can hold more stock than the pool's cap [2]. We think the bounded pool is good engineering. The set of lockable rows stays small, and the ledger remains the source of truth [6].
SKIP LOCKED is what sends concurrent checkouts to different rows. According to MySQL's locking-read documentation, the option excludes rows whose locks cannot be acquired immediately and returns other rows instead of waiting [10]. The manual describes it as a mechanism for queue-like tables [10]. It also warns that queries which skip locked rows return an inconsistent view of the data [11].
The transaction boundary matters as much as the schema. Reserve and claim each run as short database transactions [13]. The reserve transaction releases its row locks, and the stored reservation state keeps the hold in place while payment is processed [13]. In our view this is the right call for checkout. Holding a unit's lock for the length of a payment would put the next buyer back in a queue. The cost is a hold that outlives its lock. The application has to release abandoned attempts without leaving stock permanently unavailable [14].
The connection limit fits the same picture. SKIP LOCKED governs row-lock acquisition only. Other locks, connection queues and replenishment coordination can still make a request wait [15]. We'd expect a checkout that keeps a pooled connection open during slow work elsewhere to hold up reservations, however fast the reservation query itself runs. The write-up frames the case as an interaction between schema, transaction boundaries and connection occupancy [19].
The write-up does not include throughput or latency figures. Its four-unit diagram is a conceptual illustration and does not show a measured Shopify inventory count [18]. For the gains to carry over to another checkout system, two things have to be true. Contention has to spread across many units of the same item at the same location [9]. And the rest of checkout has to return database connections as promptly as the reservation transactions do [3].
What to watch
- How Shopify refills an item-location pool from the ledger as rows are claimed, given that replenishment coordination is listed as a place requests can still wait.
- Production figures from Shopify for lock waits or checkout connection hold time before and after the move.
- How abandoned holds are expired once the reserve transaction has released its locks.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence45
- Adoption30
- Hype gap0
- Incentives
- Insufficient
- Confidence50
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
Shopify moved inventory reservation from Redis into the MySQL database that already stored its inventory ledger.
- [2]
The replacement uses one row per available unit, with a pool capped at 1,000 rows per item/location combination. The ledger can represent additional stock.
- [3]
Connection hold time elsewhere in checkout became a scaling constraint even when the reservation queries were fast.
- [4]
In the old design, paid-order claim updated MySQL and cleaned up Redis through separate writes; an interrupted sequence could leave stock oversold or unavailable when it should have been sellable.
- [5]
Shopify had tried MySQL reservations before. A straightforward quantity row created a hot lock: competing checkouts updating the same row had to queue. Adding workers gave more requests a chance to wait at that row.
- [6]
Reserve begins when a customer starts payment and creates a temporary hold. Claim happens after successful payment and permanently deducts inventory from the ledger, which remains the source of truth.
- [7]
The previous model lacked location awareness; a unit is useful only if it comes from a location that can fulfill the order, and counting all stock together can produce an impossible delivery promise.
- [8]
The selection cannot cross into an ineligible group; the new selection path has to preserve the location eligibility constraint throughout the reservation.
- [9]
Each available unit gets a row in an eligible pool. A reservation chooses rows representing the quantity it needs, and concurrent transactions can choose separate units rather than modifying one shared counter.
- [10]
Per MySQL's locking-read documentation, a locking read using SKIP LOCKED excludes rows whose row locks it cannot immediately acquire and can return other rows instead of waiting; the option provides a mechanism for queue-like tables.
- [11]
MySQL explicitly warns that queries which skip locked rows return an inconsistent view of the data.
- [12]
An empty SKIP LOCKED result cannot establish that all stock is sold out; the surrounding application remains responsible for the availability decision.
- [13]
Reserve and claim use short database transactions. Stored reservation state survives payment processing after the reserve transaction releases its locks.
- [14]
The application has to connect a temporary promise to a permanent accounting operation without leaving inventory permanently unavailable when an attempt is abandoned.
- [15]
SKIP LOCKED applies to row-lock acquisition; other locks, connection queues or replenishment coordination can still involve waiting.
- [16]
There is no documented fairness or FIFO guarantee; a row can be bypassed while busy.
- [17]
The MySQL manual flags statements using SKIP LOCKED as unsafe for statement-based replication.
- [18]
The four-unit diagram is a conceptual explanation of the mechanism and does not represent a measured Shopify inventory count.
- [19]
The design is a useful example of how schema, transaction boundaries and connection occupancy interact for checkout or allocation systems.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toInventory reservation in MySQL: Shopify's row-lock design
1 article · October 9, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.
Topics
- Checkout engineeringFollow
- Inventory reservationFollow
- Database Concurrency ControlFollow