Words that inspire, ideas that spark,
stories that stay
Medusa Under Black Friday Load A capability test that turned out to be a system design problem
Akash Sharma
Wednesday, September 9th, 20266 min read
Introduction
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.
1. Two Ways To Grow a System
When one machine isn't enough, you have two options.
Vertical scaling means making one machine bigger — more CPU, more memory for the server you already have. It has a hard limit: you eventually buy the biggest server that exists.
Horizontal Scaling means running more copies of the same server and sharing traffic between them. There is no equivalent limit; you keep adding machines.
Horizontal scaling only works if the copies don't need to know about each other. That is what "stateless" means.
Stateless — 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 copies can be added or removed 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 they cannot simply be cloned. That difference is the whole design problem, and it returns in question 5.
2. 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 a load 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: 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 would be measuring the fight, not the software.
So each part of the system was given its own cores and forbidden from using any others:
|
Component |
Cores |
|---|---|
|
k6 (fake shoppers) |
2 |
|
Load balancer |
1 |
|
PostgreSQL |
2 |
|
Redis |
1 |
|
Medusa |
10 (2 per copy, so up to 5 copies) |
Because each Medusa copy gets 2 cores, ten cores means a maximum of five copies — which is why the test stops at five.
Environment
|
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 and locks |
Redis 7 |
|
Fake shoppers |
k6 |
|
Catalog |
205 products, one held at exactly 5 units |
3. 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 am not taking the same crowd and splitting it thinner across more servers — I am 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 all four ran one after another in a single sitting so nothing changed on the machine in between.
Throughput and Latency
Throughput is how many checkouts finish per minute. Higher is better. This is what the business counts.
Latency is how long a single checkout takes from click to done. Lower is better. This is what the customer feels.
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.
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.
The target set before running anything: p95 under 2 seconds per API call.
4. The Questions
Question 1 — 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 is: when we add copies, does the total keep going up?
|
Copies of Medusa |
Checkouts finished |
Compared to one copy |
|---|---|---|
|
1 |
50 |
— |
|
2 |
89 |
1.8× more |
|
3 |
126 |
2.5× more |
|
5 |
175 |
3.5× more |
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 is 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 is not Medusa falling short — it is the result of a deliberate choice we made in how we built the test, explained in question 5.
Verdict: Yes. Adding copies always increased total checkouts. Scaling out works.
Question 2 — 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.
Without a Lock: Buyer A sees 5 left and writes 4 left. Buyer B, at the same moment, also sees 5 left and also writes 4 left. Two items sold, only one removed. The shop now owes an item it doesn't have. Neither buyer did anything wrong — they simply overlapped.
With a Lock: Buyer A takes the lock, sees 5, writes 4, releases. Buyer B waits its turn, then sees 4, writes 3. Two sold, two removed — stock stays honest. A lock is a "one at a time" rule on a row: whoever holds it works alone, and everyone else queues. It costs a little waiting. It makes overselling impossible.
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 would 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.
The Test: 40 shoppers making 80 purchase attempts at one product holding exactly 5 units, spread across three separate copies of Medusa.
|
Measure |
Result |
|---|---|
|
Purchase attempts |
80 |
|
Units in stock |
5 |
|
Orders created |
5 |
|
Politely refused |
75 |
|
Oversells |
0 |
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.
Verdict: Pass. Medusa's own locking held perfectly across five independent copies.
Question 3 — Payment Processing
Payment is the step everyone assumes is the slow one. Each of the nine steps was timed separately.
|
Step |
Average time |
|---|---|
|
Pick shipping |
6.84 s |
|
Place the order |
5.09 s |
|
Enter address |
3.60 s |
|
Add to cart |
3.45 s |
|
Create cart |
2.00 s |
|
List shipping |
1.65 s |
|
Browse products |
1.10 s |
|
Payment setup |
0.95 s |
|
Payment session |
0.44 s |
The two payment steps are the fastest things in the entire checkout. Picking a shipping method is the most expensive step by a wide margin.
The reason picking a shipping method costs so much is worth knowing, because it is 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 is what stops a failed checkout leaving a half-made order behind, and it is 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 is a deliberate trade: some speed, in exchange for never leaving the data in a broken state.
There is also a design reason payment is so cheap here, and 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 would not 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.
Verdict: Pass. Payment is the cheapest part of checkout, and built so a slow provider can't block the rest.
Question 4 — 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?
|
Measure |
Value |
|---|---|
|
What one Redis can handle |
100,000+ operations/second |
|
Our peak usage |
233 operations/second |
|
Percentage of capacity used |
about 0.2% |
|
Time per operation |
about 40 microseconds |
|
Memory used |
under 4 MB |
At its busiest moment, Redis was doing 233 operations per second — while simultaneously holding every inventory lock that kept question 2 correct.
Redis was never remotely close to being a problem, and that is the right outcome. It is the one piece every copy of Medusa must share, so it needs enormous spare capacity by design. Worth monitoring; not worth worrying about.
Verdict: Pass. Used 0.2% of its capacity. Never a constraint.
Question 5 — PostgreSQL, Where The Real Limit Was
This is the most important result, and it is 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.
|
Copies of Medusa |
Medusa CPU used |
PostgreSQL CPU used |
|---|---|---|
|
1 |
1.9 of 2 cores |
0.25 of 2 cores |
|
2 |
3.2 of 4 cores |
0.57 of 2 cores |
|
3 |
4.6 of 6 cores |
0.97 of 2 cores |
|
5 |
8.0 of 10 cores |
1.94 of 2 cores |
Medusa always had headroom, because we kept giving it more cores. The database went from a quarter of one core to almost exactly full — 97% of everything it had.
That is the reason five copies gave 3.5× instead of 5×. It is not a limitation of Medusa — it is 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 are 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, there is the workflow bookkeeping from question 3 — roughly 36 operations per checkout spent recording progress so a failed order can be undone. Multiply all of 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 is 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.
Verdict: Half-scaled. The database was the limit — because we never scaled it.
Question 6 — 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 is 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.
Verdict: Signal identified. Scale on Medusa's CPU and request time — not database CPU.
Question 7 — Order consistency under load
Throughput tells you how many orders you took. This tells you whether you can trust them.
|
Measure |
Result |
|---|---|
|
Deadlocks |
0 |
|
Operations stuck waiting |
0 |
|
Clean refusals |
75 |
|
Half-made orders |
0 |
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. Counting rows in the database afterwards, the stock and the orders agreed exactly. This is the workflow bookkeeping from question 3 earning its cost.
That is 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.
Verdict: Pass. The data stayed correct even when the system was overwhelmed.
5. 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 is 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
-
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 would expect that curve to run much closer to straight. I cannot put a precise figure on it without running the test, but the 42% we got from one cache change on one step suggests there is 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.
Conclusion
Medusa does its job — it stays correct under pressure, it never oversold, and it is 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 is the part that needs the engineering, and that is where I would spend the next round of work.
Struggling to choose the right Headless CMS & Headless Commerce tech stack? We’ll help you pick the best solution for your business! Exclusive Offer: 20 Hours of Free Development & Consultation
Book a Meeting