We Made Tournament Creation 95% Faster. Here's What Was Wrong.

By Vincent Luder
Published February 12, 2026

Tournament creation used to take over a minute. Now it takes a few seconds. We rewrote the backend query pipeline, added parallel processing, and eliminated redundant API calls. Here's the full breakdown.

If you've created a tournament on HappyCharts in the last few weeks, you probably noticed something: the loading screen after clicking "Create Tournament" lasted... a while. Like, "did it crash?" a while. Sometimes 50 seconds. Sometimes over a minute and a half. You'd sit there watching the spinner, wondering if the app ate your request and went to lunch.

It didn't crash. It was just doing something incredibly stupid behind the scenes. And now it's fixed.

What Was Actually Happening

When you create a tournament, the system needs to find stock and crypto symbols that have enough historical data to generate 25 rounds of trading scenarios. Makes sense, right? Each round needs 200 data points — 100 for the chart you see, and 100 for the answer period after you make your decision.

Here's where it got ugly: the old code would grab a list of every symbol in the database, then check each one individually to see if it had enough data. And by "check," I mean it would query the database asking for up to 10,000 rows per symbol, load them all into memory, count them, and then decide "yes" or "no."

So if you had 100 symbols in the database? That's 100 sequential database queries, each fetching thousands of rows. One after the other. No parallelism. No shortcuts. Just raw, brute-force patience.

It's the kind of code that works perfectly in development when you have 3 symbols. And then you go to production with 100+ symbols and suddenly your users are aging visibly during tournament creation.

The Fix: One Query Instead of a Hundred

The solution was embarrassingly simple. Instead of fetching all the data for every symbol and counting it in Python, we now ask the database to do the counting. One query: "Hey database, which symbols have at least 200 data points for this timeframe?" The database answers in milliseconds using an index it already had.

That's it. That single change turned the symbol filtering step from 50-100 seconds into less than a second. One GROUP BY query with a HAVING COUNT(*) clause. If you're a developer reading this, you're probably shaking your head right now. Yeah, me too.

But We Didn't Stop There

While we were in there, we found two more bottlenecks.

Redundant data fetching during generation. When the system generates 25 rounds, the same symbol can appear multiple times (say, Bitcoin shows up in round 3, round 11, and round 22). The old code would fetch that symbol's entire market history from the database every single time. Now it caches the data per symbol during generation. First time? Fetch from DB. Second time? Already in memory. This cut the snapshot generation time significantly.

Sequential market data loading for the response. After creating the tournament, the API needs to attach market data to each of the 25 snapshots before sending the response back to you. The old code did this one snapshot at a time. Snapshot 1, wait. Snapshot 2, wait. Snapshot 3, wait... all the way to 25. Now all 25 fetch in parallel using async concurrency. What used to take 5-10 seconds now takes 1-2.

The Frontend Was Wasteful Too

Here's the kicker: even after the backend created your tournament and sent back all the data (including all the snapshots with their market data), the frontend would immediately throw that data away and re-fetch everything from the API. The creation response contained exactly the data it needed, and it just... ignored it. Navigated to the tournament page and made two fresh API calls to get the same information.

Now the creation response gets cached in the app's state. When you navigate to the tournament screen, it checks: "Do I already have this data?" If yes, skip the API calls entirely. If no (like when you refresh the page), fall back to the API like before. Best of both worlds.

The Numbers

What changedBeforeAfter
Symbol filtering50-100s<1s
Snapshot generation5-10s1-3s
Market data for response5-10s1-2s
Frontend re-fetch2-5s0s
Total~60-125s~3-6s

That's roughly a 95% reduction in total load time. Tournament creation now feels instant compared to before.

What This Means For You

Nothing changes about how you use HappyCharts. You click create, and your tournament appears. The only difference is that now it actually appears quickly instead of making you question your internet connection.

If you were one of the people who thought the app was broken during tournament creation — it wasn't. It was just slow. And now it's not.

Go create a tournament. Blink and you might miss the loading screen.