6 minute read

This is Part 2 of a four-part series. Part 1 covers the mission, Part 3 covers the infrastructure journey, and Part 4 covers game day itself.

Before we could scale the backend, we needed to be able to generate a Super Bowl’s worth of traffic on demand. That sounds like a smaller problem than it turned out to be.

Starting point: AWS Distributed Load Testing on AWS

We built on top of the AWS Distributed Load Testing on AWS solution rather than hand-rolling our own load generation stack. Under the hood, the solution pairs a dashboard front end with an ECS Fargate cluster that runs the actual load generation as containerized JMeter tasks — no servers to provision, and it scales out by launching more tasks rather than more hand-managed EC2 instances.

That container-per-task model matters because JMeter doesn’t scale vertically forever — a single JMeter node tops out at roughly 1,000–2,000 concurrent requests before you’re fighting the test client’s own resource limits rather than measuring your backend. The only way past that ceiling is horizontal: more containers, coordinated, feeding results back to one place.

We wrote our test plans in JMeter, since that’s what the solution is built around and it’s flexible enough to model the actual burn transaction flow — not just hammer a single endpoint with GET requests.

Why not a commercial load testing platform

We looked at commercial options like BlazeMeter before committing to this path, and passed on them for a simple reason: they’re built around contracts and usage-based pricing on top of that contract, and we didn’t know yet how much load we’d actually need to buy. AWS Distributed Load Testing on AWS is just infrastructure we already had a relationship with — Fargate tasks running in our own accounts, billed as normal AWS usage, no minimum commitment and no separate vendor negotiation to get started. That mattered more than it sounds, because our test scale grew by an order of magnitude over the course of the project, and we didn’t want a contract ceiling to be the thing that capped how far we could push.

It also meant we could start cheap. We ran small tests first — a few thousand requests, enough to prove the pipeline end-to-end and shake out obvious bugs in the test plan and the target — before committing time and account resources to the six-account, two-region setup below. Scaling a free-to-experiment tool up gradually is a much easier conversation than scaling a metered contract up gradually.

Why one account wasn’t enough

The first thing we ran into wasn’t a backend limit — it was a load generation limit. A single AWS account and region has ceilings (service quotas on things like concurrent Fargate tasks, ENIs, and elastic IPs) that cap how much simultaneous load you can throw from one place. We hit that ceiling well before we got anywhere near a number that resembled a Super Bowl audience.

Requesting quota increases only gets you so far, and it doesn’t solve the deeper issue: even with higher quotas, a single account and region is still a single blast radius and a single point of contention for the load generation infrastructure itself.

So we scaled the test harness horizontally, the same way you’d scale any other system that’s hit an account-level ceiling: run more of it, in parallel, in more places.

Spreading the test across six accounts and two regions

We ended up running the AWS Distributed Load Testing solution independently in six AWS accounts, across two regions, all firing JMeter load at the same target concurrently. Each account/region pair ran its own stack of the solution — its own test runner infrastructure, its own quotas to work with — and we coordinated launch timing so the combined load looked like a single coherent spike instead of six staggered ones.

flowchart TB subgraph R1["Region 1"] A1["AWS Account 1
Distributed Load Testing + JMeter"] A2["AWS Account 2
Distributed Load Testing + JMeter"] A3["AWS Account 3
Distributed Load Testing + JMeter"] end subgraph R2["Region 2"] A4["AWS Account 4
Distributed Load Testing + JMeter"] A5["AWS Account 5
Distributed Load Testing + JMeter"] A6["AWS Account 6
Distributed Load Testing + JMeter"] end A1 & A2 & A3 & A4 & A5 & A6 --> Target["NFT token burn backend
(3 ALBs, see Part 3)"] A1 & A2 & A3 & A4 & A5 & A6 -.results.-> Agg["Aggregated results
(CloudWatch / S3 per account)"]

This gave us a few things a single-account setup couldn’t:

  • Combined throughput well beyond what any one account’s quotas would allow
  • Two-region redundancy in the test harness itself, so a regional service issue during a test run didn’t invalidate the whole exercise
  • Isolation between test infrastructure runs, so a misbehaving test in one account (a stuck task, a runaway thread group) didn’t take down the others mid-run

The tradeoff was operational overhead — six sets of results to pull together instead of one, and coordination across accounts to make sure we were actually testing a simultaneous spike and not six sequential ones that happened to overlap on a chart. We treated the per-account CloudWatch metrics and JMeter result artifacts as inputs to aggregate after each run, rather than trying to get a single unified live dashboard across accounts.

Making the case for 1.2M to AWS

A number like 1.2M requests doesn’t sound like something you back into by accident, and we didn’t — we had to justify it. Six accounts running load tests across two regions ahead of a nationally televised event is exactly the kind of thing AWS’s special events team (their Infrastructure Event Management program, which we lean on again in Part 3) wants visibility into, and they wanted to understand why we were asking for headroom at that scale.

The case we made was Coinbase’s 2022 Super Bowl QR-code ad. That single 60-second spot — also just a bouncing QR code — reportedly drove more than 20 million hits to Coinbase’s landing page within a single minute, and the app crashed for close to an hour under the load. That’s the reference point our team used internally for the kind of instantaneous request rate a QR-code-driven Super Bowl spot can realistically generate — traffic peaking around the 1M-requests-per-second range in the worst moments, not the smoother curve a typical marketing campaign produces. We walked AWS’s team through that precedent directly: a comparable ad, on the same stage, had already taken down a well-resourced crypto company’s infrastructure at that scale, and we intended to test past it rather than find out the hard way.

That conversation is also what got us the account-level support and attention to actually run six-account, two-region tests without our own load generation tripping alarms as if it were an attack — which is a problem we ran into again from the other direction once Cloudflare was in the picture (more on that in Part 3).

Getting to 1.2M

Running the test this way, across six accounts and two regions, we were able to validate the backend against 1.2 million requests in a single coordinated test run — comfortably above what we expected (and ultimately saw) during the actual broadcast, and past the precedent that worried us going in.

That number didn’t come for free. Reaching it took multiple rounds of running the test, watching the backend fall over or degrade in a specific way, going back to fix that specific thing, and re-running. Part 3 is the story of what those rounds actually found — the load balancer swap, the CDN swap, and the IP exhaustion issue that almost caught us out.