Scaling Flash Sales: Why Shopify Migrated from Redis to MySQL for Inventory Reservations
Flash sales are the ultimate stress test for any e-commerce architecture. When millions of buyers descend upon a platform simultaneously to grab a limited-edition drop, the traffic pattern looks less like a bell curve and more like a cliff. For engineers, this introduces a brutal technical paradox: you need to decrement inventory counters with sub-millisecond latency, yet you cannot afford to oversell a single item. If your system sells 500 pairs of sneakers when you only have 50 in stock, you are looking at angry customers, manual reconciliation nightmares, and severe brand damage.
For years, the industry reflex for handling this kind of high-velocity write contention was simple: reach for a distributed cache like Redis. It is fast, in-memory, and built for atomic increments. But at the scale of a global commerce platform like Shopify, the distributed cache model starts to crack under its own complexity.
This is the story of why Shopify completely re-architected its core inventory reservation system. They moved away from a Redis-backed counter model and leaned directly into MySQL 8.0, proving that a mature relational database, when paired with the right access patterns, can outperform specialized NoSQL caches for transactional integrity. If you are building high-concurrency systems, examining how Shopify tackled this migration offers a masterclass in database architecture, concurrency control, and the ongoing quest to simplify the tech stack.
The Redis Trap: Why Distributed Caches Fall Short on ACID
Historically, Shopify handled flash-sale inventory reservations by relying heavily on Redis. The pattern is common across the industry: maintain a high-speed counter in a distributed cache to handle the furious read-and-decrement loop during a drop, and asynchronously sync those state changes back to a permanent relational database ledger (like MySQL) in the background.
On paper, this sounds reasonable. Redis handles the IOPS throttling, keeping the primary database safe from a barrage of concurrent writes. In practice, however, this architecture introduces a fatal flaw: the lack of ACID atomicity across the boundary between the cache layer and the permanent ledger.
“Decoupling the reservation cache from the permanent data store creates a distributed consistency nightmare where race conditions routinely result in ghost inventory and overselling bugs.”
Consider what happens when network latency spikes, a Redis shard fails over, or an asynchronous background worker drops a message. The cache says you have three items left; the database says you have zero. During a flash sale where thousands of requests hit within a 50-millisecond window, these minor synchronization gaps compound rapidly. You end up with two massive failure modes:
- Overselling: The cache allows multiple concurrent checkout threads to decrement past zero because the local check-and-set operations lack true database-level transactional isolation.
- Ghost Inventory: Orders fail or time out during checkout, but the rollback logic fails to properly increment the Redis counter back, permanently stranding inventory that legitimate buyers could have purchased.
To eliminate this class of consistency bugs, Shopify needed a system where the reservation is the transaction. They needed to move away from eventually consistent caches and back to a transactional ledger. But doing that in a relational database meant confronting the notorious bottleneck of row-level locking.
Re-architecting for ACID: The ‘One Row Per Unit’ Bounded Pool Model
If you take a traditional relational approach to inventory—say, a single row in an items table with a quantity column set to 500—and hit it with 50,000 concurrent requests, you immediately run into a wall. Every transaction tries to acquire an exclusive write lock on that exact same row. Database connection threads queue up, lock wait timeouts spike, and throughput flatlines.
To make MySQL viable for extreme flash-sale traffic, Shopify had to abandon the traditional quantity-column anti-pattern entirely. Instead, they implemented a ‘one row per unit’ design strategy.
Instead of storing an item as a single row with an integer quantity, every individual physical unit of inventory is represented as its own discrete row in the database. If a flash sale features 1,000 units of a specific sneaker, there are 1,000 distinct rows inserted into the inventory table for that item.
To prevent this table from growing infinitely or causing massive table scans during lookups, they bounded the model:
“The system uses a bounded pool of 1,000 rows per item and location combination, perfectly balancing granular concurrency with query performance safeguards.”
When a buyer initiates a checkout, the system does not update a shared integer; it attempts to claim an available row belonging to that product pool. By distributing the write contention across 1,000 distinct rows rather than concentrating it on a single counter column, row-level lock contention drops dramatically. Multiple transactions can lock different rows concurrently without blocking each other.
Of course, introducing thousands of rows per item brings its own database management hurdles, particularly around query efficiency and multi-tenant scaling. This is where modern RDBMS features come into play.
Unlocking Performance: MySQL 8.0, SKIP LOCKED, and Isolation Levels
Switching to a row-pool model was only half the battle. To actually execute concurrent claims against those rows without grinding the database to a halt, Shopify leveraged specific performance primitives introduced in MySQL 8.0.
In older database workflows, when multiple transactions attempt to query and lock rows using a SELECT ... FOR UPDATE statement, contending transactions will block and wait for the lock to release. Under heavy flash-sale load, this creates a devastating queue backlog.
MySQL 8.0 changed the game with the SKIP LOCKED modifier. When a checkout worker executes a query to claim an available inventory unit, it can instruct MySQL to bypass any rows that are currently locked by other transactions:
SELECT id, item_id, status
FROM inventory_units
WHERE item_id = 4289 AND status = 'available'
LIMIT 1
FOR UPDATE SKIP LOCKED;
Instead of waiting or throwing a lock wait timeout, the transaction immediately grabs the next available, unlocked row. This single feature completely transforms how high-concurrency write queues behave, pushing throughput sky-high.
Furthermore, managing transaction isolation levels was critical to preventing performance degradation. By running operations under the READ COMMITTED isolation level rather than the stricter REPEATABLE READ, MySQL minimizes gap locks and supremum lock contention. This prevents transactions from locking ranges of index records, drastically reducing the blast radius of locks and allowing non-conflicting inserts and updates to breeze through unimpeded.
Composite primary keys were also employed to handle multi-tenant location scaling seamlessly, allowing queries to target specific warehouse or fulfillment center partitions instantly without scanning unrelated data.
Overcoming the Real Bottleneck: Connection Exhaustion and ProxySQL
When architects talk about database migrations, the conversation usually centers around query tuning and index strategies. However, Shopify’s engineering team encountered an infrastructure bottleneck that had very little to do with InnoDB or MySQL storage engines: connection exhaustion.
During a major flash sale, the sheer volume of incoming HTTP requests translates directly into application threads attempting to open database connections. If a transaction lifecycle is poorly managed—or if application code holds onto a database connection longer than necessary while waiting on external network calls—the database pool saturates instantly. Once max connections are reached, new requests fail entirely, cascading into a total outage even if the database CPU is sitting at a comfortable 40%.
To solve this, Shopify introduced ProxySQL sitting as an intelligent middleware layer between the application tier and the MySQL clusters.
| Challenge | Redis Architecture | MySQL + ProxySQL Architecture |
|---|---|---|
| Data Consistency | Eventual consistency; prone to split-brain and sync drift | Strict ACID guarantees; single source of truth |
| Contention Point | Single-key bottleneck under massive increments | Distributed across a bounded pool of 1,000 rows per item |
| Concurrency Control | Lua scripts or custom locking logic | MySQL 8.0 SKIP LOCKED primitives |
| Connection Management | Client-heavy connection pooling | ProxySQL connection multiplexing and query routing |
ProxySQL provided critical connection visibility, pooling, and attribution. By multiplexing incoming application connections onto a tightly managed pool of backend database connections, it prevented connection storms from overwhelming the MySQL instances.
Before rolling this out to millions of live merchants, the team deployed rigorous shadow mode testing. By duplicating live production traffic and running it silently against the new MySQL reservation pipeline, engineers could validate concurrency limits, measure lock contention, and verify correctness under real-world flash-sale loads without risking a single dime of merchant revenue.
Future Outlook: The Trend Toward Simplifying the Tech Stack
Shopify’s successful migration from a complex Redis-MySQL hybrid to a unified MySQL 8.0 architecture signals a broader, highly pragmatic shift in modern distributed systems engineering.
For over a decade, the default industry playbook for scaling writes was automatic over-engineering: when relational databases hit a wall, immediately abstract them away with a specialized NoSQL data store, add a caching layer, and deal with the eventual consistency headaches later.
We are now seeing a healthy pendulum swing in the opposite direction. Mature relational databases like MySQL and PostgreSQL have evolved rapidly, introducing powerful primitives—like SKIP LOCKED, sophisticated query parallelization, and robust JSON support—that bridge the gap between transactional safety and high-velocity throughput.
When you can achieve the scale you need using a relational database, you eliminate entire categories of distributed systems failure modes. You remove dual-write synchronization bugs, simplify monitoring, reduce infrastructure footprint, and make your data model vastly easier for engineers to reason about.
As we look toward the future of enterprise architecture—whether exploring advanced engineering paradigms like those found in context engineering for AI root-cause analysis or planning large-scale infrastructure overhauls reminiscent of a post-quantum enterprise API migration roadmap—the core lesson remains consistent: simplicity and correctness beat clever architectural complexity every single time.