← All posts
Best practices

The on-sale is a load test — you only choose whether to run it first

Every on-sale is a load test that runs whether you scheduled one or not. The only thing you get to decide is whether you run it in private first or let your customers run it for you at 9am on release morning.

Look at what 2026 has already served up. FIFA's World Cup ticket site buckled under its last-minute sales phase, leaving fans staring at an endless "In Queue" screen while the rush overwhelmed the platform. When the ICC opened phase-two sales for the T20 World Cup, the ticketing platform's server crashed within minutes of going live. These are two of the biggest events on the planet, real budgets and real ops teams, and both found their ceiling the same way: live, in front of everyone who wanted in.

You don't sell World Cup tickets. It doesn't matter. Every product has its on-sale: launch day, the promo email to a full list at once, the TV mention, the invoice run on the first of the month. Whatever it is, that surge arrives faster than steady-state traffic ever does, against infrastructure whose ceiling you've only guessed at.

Model the surge, don't imagine it

"We can probably handle it" is a hunch. You turn a hunch into a capacity number by reproducing the shape of the surge first. Surges don't arrive as a flat plateau. Everyone shows up at once, so what you get is a steep ramp, which means you model it with ramp stages rather than by hammering a single concurrency figure. Step the virtual-user count up through levels that bracket your forecast peak (say 500, 1,000, 2,500, 5,000, 10,000), holding each one long enough for a stable reading before you climb again. What the climb buys you is what a single peak number can't: not just whether you survive the peak but where you stop coping.

Two questions hide inside the word "load," and they need different instruments. The first is raw volume: can the backend take the request count? That's protocol-level traffic, requests fired at your endpoints as fast as the generator produces them, cheap to scale to tens of thousands of virtual users because each one is just a request loop. It finds where your API tier, connection pool, or queue backs up. The second question is the one an on-sale actually turns on: is the app still usable at that volume? A backend returning 200s in 300ms can still ship a checkout page that takes eight seconds to become interactive, the client hydrating a SPA and rendering a full cart while the server looks fine, and that only surfaces when a real browser runs the real page. Run protocol volume to find where the backend bends, then send real-browser users through the purchase journey to confirm the front end holds up too; the load testing walkthrough covers configuring the ramp.

Gate on the tail, because the average will lie to you

Your load test reports "average response time: 280ms under peak" and everyone relaxes. That's the trap. The average gets dragged down by the fast majority, and in doing so it buries the slow tail, which is exactly the part that fails during a spike.

Percentiles are what you gate on instead. p95 means 95% of requests were at least that fast and 5% were slower. p99 is your unluckiest one-in-a-hundred, and at on-sale volume that slice is not a rounding error. Serve 200,000 requests in a launch hour and p99 is the lived experience of two thousand people, disproportionately the ones with the fullest carts. The tail also degrades non-linearly: a system holding p50 = 120ms steady can watch p99 climb from 400ms to eight seconds while the mean barely twitches. We wrote up the statistics behind reading percentiles if you want the SRE-grade version.

So don't just watch the tail. Gate on it. A load test should have pass/fail thresholds the same way a unit test has assertions:

export const options = {
  thresholds: {
    http_req_duration: ['p(95)<1000'], // 95% of requests under 1s
    http_req_failed:   ['rate<0.01'],  // error budget: under 1%
  },
};

A run passes only if every gate holds for the whole run. A threshold that comes out true on average but gets violated for a 90-second window still failed, because that window is a real outage that real people sat through. Set thresholds from a passing baseline run rather than a guess, then tighten them as results stabilize.

Find the knee

Every system has a knee, the concurrency level where latency stops rising gently and goes vertical. Below it, each new user costs a few milliseconds. Above it, each new user costs the whole system, because you've saturated something and requests start queuing. You find the knee where p95 and p99 fan out and climb while throughput plateaus even as you add virtual users. That inflection point is your real capacity, the only honest answer to "can we handle the on-sale." Say your forecast peak is 5,000 concurrent buyers and the knee sits at 3,000. That's an event that fails at 60% of expected traffic, and the good news is you found it on a quiet Thursday with time to fix it rather than in your mentions.

Test the degraded experience, not just the happy one

Here's what the FIFA and ICC failures really expose, and it's the part most load-test plans skip. The question isn't only whether it stays fast. It's whether, when it can't stay fast, it degrades the way you designed it to.

At true on-sale scale, some degradation is a given, so the design decision is what your users hit when it happens. Take that endless "In Queue" screen fans got stuck on. A queue is a perfectly legitimate answer to a surge, a pressure valve that admits buyers in controlled waves. But a queue is also a feature, and features have bugs. Does the page actually hold thousands of waiting users, or does it silently strand them? During your most important hour that waiting page is the most-viewed screen in the app, so it earns a test more than your happy path does.

Then there's correctness under contention, where a surge quietly costs real money. When ten thousand people race for limited inventory, does checkout stay correct? Do you oversell the last hundred tickets because two requests both read "available" before either one wrote "sold"? Do you double-charge a card because a buyer staring at a frozen spinner hit submit three times? These non-idempotent bugs only surface under concurrency, and a functional test run by one user at a time never catches them. You have to drive the contended path with many simultaneous browser users to see the race.

That last detail is not optional. If your concurrent buyers all check out as test@example.com, they'll collide on a uniqueness constraint or trample each other's state, and you've tested a bug in your harness instead of one in your app. Every simulated buyer needs distinct, per-run test data: a unique email, a unique cart, a unique order, so the collisions you observe are genuinely your app's.

Fix, then re-run — the loop is the point

One load test is a data point; the value is in the loop. Model the surge above forecast peak. Read the percentiles, meaning the tail under sustained load rather than a single burst. Locate the bottleneck: the test tells you when it breaks, and your traces and slow-query logs tell you why. Then fix it: a connection in the pool, a cached hot query, a queue page that actually holds, an idempotency key on checkout. Re-run the identical test, same script and same ramp and same thresholds, and confirm the knee moved. Repeat until the knee sits above forecast peak with room to spare, because forecasts are wrong and on-sales run hotter than modeled. The grid operators who model demand before the heat wave never skip that re-run, because a ceiling you validated last month is not the one you have after this week's deploy.

Your on-sale is coming, maybe on a date you already have circled. It won't care whether you rehearsed. All it settles is whose rehearsal it turns out to be: yours, in private, with time to fix what broke, or your customers', in public, with your product's name on the outage.

Run it first

TestVibe models both ends of the surge from one place: protocol-level k6 volume with ramp stages and threshold gates, plus real-browser users that replay your actual purchase journey as concurrent buyers, each with unique data, so you see whether the app stays usable and correct under contention rather than only whether the server returned a 200.

Want to run your on-sale before your customers do? Get early access, or read the load testing walkthrough for the full flow.

Early access

Ready for tests that write themselves?