Medusa Under Black Friday Load
A capability test that turned out to be a system design problem.
Good morning. My name is Akash Sharma, and I've been an intern at Weframe Tech for about five months.
When I was asked to write a technical blog, my first topic came back rejected — not deep enough, they wanted something more substantial. I don't have years of experience to draw on, so I picked a subject that sounded genuinely interesting and decided to learn it properly.
The question I chose was: can Medusa handle a Black Friday checkout sale?
From the outside that looks like a test of Medusa, and for part of it, that's exactly what it is. Some of the things I was asked to examine — inventory locking, order consistency — really are questions about the platform. Medusa answers those well. In every test I ran, it never oversold a single unit.
But the headline part of the question isn't about Medusa at all. Running hundreds of thousands of checkouts at the same time is not something any single machine does, however good the software on it is. That part is a system design question: how many machines, arranged how, with what underneath them.
So that's what I built a test for.
Two ways to grow a system
When one machine isn't enough, you have two options. Only one of them works here, and understanding why is the foundation for everything that follows.
Horizontal scaling only works if the copies don't need to know about each other. That's what "stateless" means.
Stateless — and why it matters here
A stateless service remembers nothing between requests. It keeps no data of its own in memory, so any copy can serve any customer at any time, and you can add or remove copies freely.
Medusa's API is stateless. Every piece of durable information — your cart, your order, the stock count, your session — lives in the database or in Redis, never inside the Medusa process. So if your "add to cart" is served by copy #1 and your "checkout" by copy #4, it doesn't matter: both read the same cart from the same database.
The database and Redis are the opposite. They are the memory, so you can't just clone them. That difference is the whole design problem, and it comes back in question 5.
The lab
I built a complete Medusa v2 store and sent real checkout traffic at it: browse the catalogue, create a cart, add an item, enter addresses, pick a shipping method, create a payment session, and complete the order. Nine API calls — exactly the ones a real storefront makes.
How a request travels. k6 is the tool that pretends to be shoppers — it fires the same nine API calls a real customer would, over and over. Those requests hit the balancer, which hands each one to the next Medusa copy in turn so the work is spread evenly. Whichever copy receives it does the same thing: read and write to the one shared PostgreSQL database, and use one shared Redis for locks and caching.
The important detail is on the right. However many Medusa copies we run, there is only ever one database and one Redis underneath all of them.
Everything runs on one laptop — so how is that fair?
A modern CPU is divided into cores — independent workers that each handle one stream of instructions at a time. My machine has 16. Normally every program competes for all of them, which would be a problem here: if the fake-shopper tool grabbed cycles from the database, I'd be measuring the fight, not the software.
So I gave each part of the system its own cores and forbade it from using any others:
2 cores for the fake-shopper tool · 1 for the balancer · 2 for PostgreSQL · 1 for Redis · 10 left for Medusa. Because each Medusa copy gets 2 cores, ten cores means a maximum of five copies — which is why the test stops at five.
| Component | Detail |
|---|---|
| Host machine | AMD Ryzen 7 5800HS · 16 cores · 16 GB RAM |
| Commerce platform | Medusa v2.17.2, production build, Node 22 |
| Database | PostgreSQL 16 |
| Cache & locks | Redis 7 |
| Fake shoppers | k6 |
| Catalog | 205 products, one of them held at exactly 5 units |
The test
I ran the same test four times — with 1, then 2, then 3, then 5 copies of Medusa — and counted how many checkouts finished.
Each copy was given 10 shoppers of its own. So one copy served 10 shoppers, two copies served 20, three served 30, and five served 50. This matters: I'm not taking the same crowd and splitting it thinner across more servers — I'm growing the crowd along with the fleet, which is what actually happens on Black Friday. If five copies really do five times the work, five copies should finish roughly five times as many checkouts.
Each run lasted four minutes, and I ran all four one after another in a single sitting so nothing changed on the machine in between.
Two different numbers come out of a test like this, and it's worth being clear that they measure different things.
Throughput and latency
They can move in opposite directions. A shop can be finishing plenty of orders per minute while every individual customer waits far too long. You need both numbers to know how you're doing.
p95 — how we measure "how bad does it get"
If you take every request and sort them from fastest to slowest, the p95 is the one sitting 95% of the way down the list. In plain terms: 95 out of every 100 requests were faster than this; 5 were slower.
We use it instead of the average because averages hide the worst cases. If 99 requests take one second and one takes a hundred, the average says two seconds and sounds fine — but someone had a terrible experience. On Black Friday, "the slowest 5%" is thousands of real people, so it's the number worth committing to.
The target I set before running anything: p95 under 2 seconds per API call.
The questions
Can it handle hundreds of thousands of checkouts?
You cannot generate Black Friday on one laptop, and any benchmark claiming otherwise is inventing numbers.
But you don't have to. This is exactly what horizontal scaling is for. If one copy of Medusa handles a certain amount of work, and adding copies keeps increasing the total, then reaching Black Friday volume becomes a matter of running enough copies — a question of budget and architecture rather than a limit in the software. So the useful test isn't "can one laptop survive a million shoppers." It's "when we add copies, does the total keep going up?" That I can measure honestly.
Every copy we added finished more orders than the fleet before it. Nothing plateaued and nothing went backwards. That is the answer to the question: Black Friday volume is reachable by running more copies, so it's a matter of provisioning rather than a wall in the platform.
The one imperfection is that five copies gave 3.5× rather than a full 5×. That gap isn't Medusa falling short — it's the result of a deliberate choice we made in how we built the test, and question 5 shows exactly what it was and how to fix it.
Inventory locking
This is the scenario every commerce engineer has a bad dream about: more buyers than stock, everyone clicking at the same instant.
The danger is overselling — taking payment for items you don't actually have. It happens because two customers can read the stock count at the same moment, before either has finished buying.
Medusa does this for you. Its checkout takes the lock, reserves the stock, and either commits the reservation when the order completes or gives it back if anything fails. We wrote none of that.
The one thing we configured was where Medusa keeps its locks. With five separate copies of Medusa running, a lock held in one copy's own memory would be invisible to the other four — and they'd happily oversell around it. So Medusa is pointed at Redis, which all five copies share. Redis is the single notebook every copy writes its locks into, so a lock taken by copy #1 is immediately visible to copy #4.
To test it, I sent 40 shoppers making 80 purchase attempts at one product holding exactly 5 units — spread across three separate copies of Medusa, so the copies genuinely had to coordinate through Redis rather than just with themselves.
Checked directly in the database afterwards: five units stocked, five reserved, exactly five order lines. The 75 unsuccessful shoppers got a proper "out of stock" response — not a timeout, and not a half-created order.
Payment processing
Payment is the step everyone assumes is the slow one. I timed each of the nine steps separately to find out.
The reason picking a shipping method costs so much is worth knowing, because it's the same thing that makes the whole checkout expensive. Medusa runs every checkout step as a workflow — a recorded sequence of operations. As each step runs, Medusa writes its progress to the database, so that if something fails halfway through, it knows exactly what to undo. That's what stops a failed checkout leaving a half-made order behind, and it's the machinery behind the clean rollbacks in question 7.
It isn't free. Looking at which queries consumed the most database time across the whole test, updating workflow progress was the single most expensive query of all — and the workflow table was read more than four thousand times across 126 checkouts. Roughly 36 database operations per checkout exist purely so Medusa can undo the order safely. That's a deliberate trade: some speed, in exchange for never leaving the data in a broken state.
There's also a design reason payment is so cheap here, and it's worth understanding because it holds true in production too. Medusa doesn't wait for the payment to clear before finishing the order. It creates a payment session, completes the order, and then the payment provider confirms separately a moment later. So even when the provider is slow, that slowness sits in one isolated step instead of holding the whole checkout open.
What this test does and doesn't tell you. We used Medusa's built-in test payment provider, which approves instantly without contacting anyone. So these numbers measure Medusa's side of payment — the work it does to create and record a payment — and that side is genuinely fast and well-designed. What they don't include is the trip out to Stripe or Razorpay, which typically adds a few hundred milliseconds. Adding that would push the payment step up, but it wouldn't change the ranking: payment would still be far from the most expensive part of this checkout, and thanks to the design above, a slow provider delays one step rather than everything.
Redis performance
Redis is a very fast store that keeps everything in memory rather than on disk. In our setup it does two jobs: it holds the inventory locks from question 2, and it caches data so Medusa doesn't have to re-fetch it. Because all five copies of Medusa share it, if Redis were slow, everything would be slow.
So the question is simply: did Redis ever struggle? One Redis instance can comfortably handle over 100,000 operations per second. Here's what we actually asked of it.
Redis was never remotely close to being a problem, and that's the right outcome. It's the one piece every copy of Medusa must share, so it needs enormous spare capacity by design. Worth monitoring; not worth worrying about.
PostgreSQL — where the real limit was
This is the most important result, and it's a lesson about how we built the test.
We added copies of Medusa and left the database completely untouched. One PostgreSQL, two cores, default settings, from the first run to the last. Five copies of Medusa, all sending their work to the same single database.
That red bar filling up is the reason five copies gave 3.5× instead of 5×. It isn't a limitation of Medusa — it's arithmetic. Five times the traffic arrived at a database that never got any bigger.
What makes the database work so hard is that a single checkout runs about 394 database queries. Not errors — successful queries, each taking a millisecond or two. They're that numerous because Medusa is built from separate modules (products, pricing, cart, inventory, payment, orders), and each module fetches its own data rather than sharing one big combined query. Browsing 20 products alone touches products, variants, options and prices as separate lookups. On top of that, Medusa records every step of the checkout so it can undo the order safely if something fails partway. Multiply that by every copy in the fleet and the database gets busy fast.
As a quick check, I added a cache in front of the product listing so repeat browsing didn't hit the database at all. Completed checkouts rose 42% immediately. That's a useful confirmation: take load off the database, and the whole system speeds up.
The honest conclusion is that we only scaled half the system. To straighten out that curve, the database has to grow alongside Medusa.
Autoscaling
Autoscaling means not deciding by hand how many copies to run. You set up a watcher that keeps an eye on one measurement, and when that measurement crosses a line you've drawn, it starts extra copies automatically — then shuts them down again when traffic drops. It's how a shop survives a sale without paying for a huge fleet all year.
The hard part is choosing which measurement the watcher looks at. The most common choice is database CPU — and our results show that would have been a bad decision here.
| What the watcher could measure | Reading with 1 copy | Would it have acted? |
|---|---|---|
| Database CPU — the usual choice | 0.25 of 2 cores | No |
| Medusa's own CPU | 1.9 of 2 cores | Yes |
| How long requests take | 13.6 s | Yes |
With one copy running, the database looked almost idle while customers were waiting thirteen seconds. A watcher pointed at the database would have concluded everything was fine and done nothing. Watch Medusa's CPU and how long requests are taking — those are the two that noticed immediately.
One warning that follows from question 5: every new copy also adds load to the database. So autoscaling the Medusa side without a plan for the database eventually creates the problem we hit — the extra copies meant to fix slowness are what fill the database up.
Order consistency under load
Throughput tells you how many orders you took. This tells you whether you can trust them.
A deadlock is when two operations each wait for something the other holds, and neither can ever continue — the database has to kill one of them. We had none, at any fleet size, in any run.
More importantly, the 75 shoppers who didn't get an item during the stampede were each rolled back completely. No order was created without stock behind it; no stock was set aside for an order that never existed. When I counted rows in the database afterwards, the stock and the orders agreed exactly.
That's the outcome that matters. Under heavy load this system got slower, but it never got things wrong — and those two failures are not equally bad. Slowness costs you some sales that day and is fixed by adding machines. Incorrect stock means shipping items you don't have, refunds, apologies, and customers who don't come back.
In summary
Can Medusa handle a Black Friday checkout sale? Yes — and the questions split cleanly into two groups.
The parts Medusa is responsible for, it handles well. Inventory locking, order consistency, payment design and its use of Redis were all correct under real pressure. Eighty shoppers raced for five units across three separate copies and got exactly five orders, with no deadlocks and no phantom stock. That's the failure that would genuinely hurt on the biggest day of the year, and it never happened.
The parts system design is responsible for are where the work is. No single machine serves Black Friday, so the real question was whether capacity grows when you add copies — and it does. Every copy we added finished more orders than the fleet before it. We got 3.5× from five copies rather than a full 5×, and that shortfall was our own doing: we grew Medusa and left one small database serving all of it.
What we'd do next, and what it should buy
Three concrete changes, in order of expected impact:
- Give the database more resources. It ran on two cores the entire time and finished at 97% full. This is the single biggest constraint and the cheapest thing to change.
- Add a connection pooler in front of it. Every Medusa copy opens its own set of connections, and PostgreSQL handles each one as a separate process. A pooler lets many copies share a small number of real connections, so the fleet can grow without the database drowning in connections.
- Reduce the 394 queries per checkout. Caching the product listing alone lifted completed checkouts by 42%. Extending that idea — caching more of the read-heavy steps, and letting copies of the database serve reads — attacks the root cause rather than the symptom.
With the database scaled alongside Medusa, I'd expect that curve to run much closer to straight. I can't put a precise figure on it without running the test, but the 42% we got from one cache change on one step suggests there's a lot of room.
The target we missed
We set out to keep p95 under 2 seconds per request. Our best result was 13.6 seconds — roughly seven times over. Part of that is the machine: a single laptop was running the fake shoppers, the database, Redis and five copies of Medusa all at once, which is not how any of this would be deployed. But part of it is the database bottleneck above, and that part is fixable.
So the answer to the question I started with is this. Medusa does its job — it stays correct under pressure, it never oversold, and it's built to run as many copies as you need. What decides whether a store survives Black Friday isn't Medusa. It's whether the system around it — the database, the caching, the connection handling, the scaling rules — was designed to grow with it. That's the part that needs the engineering, and that's where I'd spend the next round of work.