Shopify moved the inventory reservation system that backs its checkout flow off Redis and onto MySQL, and the migration held up through a Black Friday peak. The engineering team wrote up the details on the Shopify Engineering blog, and the writeup is worth reading if you run anything with hot rows and high contention.
The reservation system is the part of Shopify’s platform that, when a shopper adds an item to their cart, holds stock so two people do not buy the last unit at the same time. At Shopify’s scale it handles a huge share of the world’s e-commerce traffic, which makes it a demanding workload: short requests, extreme read and write rates, and no room for double-selling a product.
Why move off Redis
Redis handled the raw throughput fine. The problem was correctness. The old design used Redis scripts and locks to coordinate reservations, and keeping the reservation state consistent with the inventory ledger that sits behind it was getting tangled. The team wanted the atomicity guarantees that a relational database with real transactions gives you, where a reservation and its ledger entry either both commit or neither does.
So they rearchitected the system around MySQL 8, using a single relational store for both reservations and the underlying inventory ledger. That removes the gap between the hot cache and the source of truth. One ACID transaction now covers the whole operation, which kills a whole class of overselling and underselling bugs that come from state diverging between two systems.
The schema and SKIP LOCKED
The interesting part is how they modeled the data. Instead of storing a quantity counter on a single row per product, Shopify stores one row per physical unit of inventory. A bounded pool of around 1,000 rows exists for each item and location combination. When a reservation comes in, the system claims an available row using MySQL’s SKIP LOCKED clause.
That is the key trick. A naive SELECT ... FOR UPDATE would make each checkout wait on whatever transaction currently holds the lock on the hot row, which collapses under a flash sale. SKIP LOCKED tells the database to skip rows that are locked by another transaction instead of waiting on them, so concurrent reservations for the same item each grab a different free row and proceed in parallel instead of queueing behind one another.
The real bottleneck was connections
The most surprising finding in the writeup is that the database itself was not the limiting factor. Once the schema was in place, the team discovered that connection pool attribution was the actual bottleneck. Requests were sitting around waiting for a database connection to free up rather than doing useful work, and the tooling could not tell which part of the system was holding those connections.
They built connection visibility tooling that attributes each connection to the code path that took it, and fixing the queuing around that reduced wait times by roughly half. It is a good reminder that in a high QPS system, the database is often waiting on the application’s connection handling more than the application is waiting on the database.
Rollout in shadow mode
Shopify did not flip a switch. They ran the new MySQL system in shadow mode first, sending real traffic through both the old and new implementations and comparing the results before cutting over. That gave them confidence the new system produced correct reservations before it handled any production load. Then they cut over, kept the Redis path around as a fallback, and let Black Friday traffic prove it out.
The results speak for themselves. The system sustained around 200,000 queries per second using SKIP LOCKED, and Shopify handled its stated peak of $5.1 million in sales per minute during the Black Friday 2025 window without the reservation layer becoming the bottleneck.
What this means for developers
There are a few takeaways that travel well beyond Shopify’s specific setup.
First, Redis is not automatically the right answer for hot data. A cache with a source of truth behind it is two systems to keep consistent. If your workload genuinely fits a relational model and the hot rows are bounded, a modern MySQL setup with SKIP LOCKED can replace both layers and remove a whole class of consistency bugs.
Second, design for the actual contention pattern. The one-row-per-unit model is a deliberate choice for high-contention stock, and it spreads the writes across many rows instead of funneling them into one. That matters more than raw engine speed for a workload where the same item is being reserved thousands of times a second.
Third, measure the connection layer before you blame the database. In many systems the free connection count is the real limit, and you will not find it by staring at slow query logs.
When to consider the same move
This pattern is not for every system. If your data does not change often, or you genuinely just need a fast key-value read path and eventual consistency is acceptable, Redis remains the simpler tool. The trade pays off when you need strong consistency on frequently mutated, hot rows and you want to collapse a cache plus database into one system to make correctness tractable.
If you are considering something similar, the block-level details worth copying are the bounded row pool per item, the use of SKIP LOCKED instead of FOR UPDATE, connection attribution tooling added before you trust your capacity numbers, and a real shadow-mode cutover. Together those are a complete playbook for moving a hot, correctness-sensitive workload onto a relational store.