Launch Day: 1000 Players, One Incident, and Five Quick Improvements
By Ergo Posti ·

Yesterday the World Cup 2026 kicked off, and so did Resultigo, for real. We had been testing with friends for weeks, but the opening match was our actual launch: more than 1,000 players made their predictions on day one.
A real launch is humbling. Things broke that had never broken in testing, and people asked for things we had never thought of. This post is the honest recap: one incident, how we fixed it, and five improvements that shipped because real players asked for them.
The Incident: We Rate-Limited Our Own Users
At 8:58 PM, about an hour before the opening kickoff, users started seeing "Too many requests" errors. The strange part: our servers were perfectly healthy. CPU, memory, response times, all green. The app wasn't overloaded at all. It was cheerfully refusing to serve people.

What is rate limiting, and why did it go off?
Every public website protects itself with a rate limiter. It works like a bouncer: it counts how many requests each visitor makes and turns away anyone asking for absurdly many. It tells visitors apart by their internet address, the IP. Our limits were generous: with 1,000 players, each person should have had thousands of requests to themselves.
The catch: traffic to our API doesn't arrive directly. It first passes through Cloudflare's network, then through our hosting provider's load balancer. The Cloudflare part isn't something we set up ourselves: our hosting provider routes all of its apps through it automatically, to shield them from attacks and speed up delivery. So there are two middlemen between a player and us, but our server was set up to expect just one. Because of that one wrong number, it never saw the real visitor's address. It saw the address of the Cloudflare data center the request came through. And since most of our players are in Estonia, almost everyone entered through the same handful of data centers.
So instead of 1,000 players each having their own allowance, the bouncer saw what looked like five visitors making thousands of requests each. Everyone was wearing the same name tag. The math finally made sense: a single page load makes about five requests, so a couple hundred page views per minute across all players combined was enough to use up the shared allowance. Match-day traffic did exactly that.
The quick fix: buy breathing room
With kickoff approaching, we didn't yet know any of the above. To make it more interesting, the whole team was mobile, already on the way to watch the opening match somewhere. We spotted the errors at 9:00 PM, an engineer reached a laptop by 9:28 PM, and at 9:36 PM, 24 minutes before kickoff, we shipped the pragmatic thing: all limits raised about 10x. We knew we were treating the symptom, not the cause. It got players through the match, but it didn't explain why limits were being hit at a fraction of the expected load. And it weakened protections we actually want, like the login limit that defends against password guessing.

The root cause: one wrong number
The raised limits held through match day, and meanwhile we dug in properly. A quick DNS lookup of our domain showed the extra Cloudflare layer our server didn't know about.
The request logs confirmed it. Every request seemed to come from a Cloudflare address, and real visitor addresses never showed up. One log line was especially satisfying: a player behind a corporate proxy arrived through three middlemen, not two. The chain isn't even the same length for everyone. That's why the fix counts middlemen from our end of the chain: the visitor's end can be faked, so it can't be trusted.
The actual fix was one line: tell Express there are two trusted proxies in front of it instead of one.
app.set("trust proxy", 2); // Cloudflare edge → Render load balancerWith that, the server sees the real visitor behind every request, and every player gets their own allowance again. One line fixed all three rate limiters and the request logs at once. Next up: bring back the original stricter limits, since the raised ones are needlessly loose now, and add an automated check that warns us if the addresses ever stop matching what Cloudflare reports.
What we learned
- "Rate limited, but the server is healthy" means the limiter is counting wrong, not that traffic is too high.
- Check your assumptions about the path a request travels. One DNS lookup revealed a middleman the code didn't know about.
- We did load test before launch, but with the rate limits raised out of the way. The one piece of the system the test skipped was exactly the piece that failed.
Five Improvements, Courtesy of Real Players
The incident was only half the story. A thousand real players use an app very differently from a friendly test group, and the feedback came fast. Here's what we shipped in response, all of it live before the second match day kicked off.
1. Fair rankings when players are tied
After the first match, several players had identical points. Our leaderboard handled ties the wrong way: if three players shared 1st place, the next player was shown in 2nd. In standard sports ranking, they should be 4th as there are three people ahead of them! We switched to proper competition ranking, the same system used in actual football tables.

2. A ranking chart that survives 400 players
Our public World Cup league grew to almost 400 members, far more than we ever tested with. The ranking history chart drew a line for every single player, and the name labels completely buried it. The fix: the chart now shows the top 15 players, which keeps it readable at any league size.

3. See the whole leaderboard, not just the top 5
The leaderboard showed the top 5 players plus your own position, so you could always see yourself, but not the players around you. "How am I doing compared to others?" and "Where's the full leaderboard?" were common questions. Now you can expand the leaderboard and see every player in the game.
4. See what everyone else predicted
This was the most requested feature of the day. Once bonus questions lock, players wanted to see what everyone else answered. Half the fun is comparing picks. We built it the same day: after lock-in, all answers are visible in the dashboard table.

5. An about page for leagues with prizes
Some leagues play for real prizes and wanted a place to explain their rules and rewards to members. Owners of public and premium leagues can now add a description page to their game.

Two Matches Down, 102 to Go
And because we love numbers, here's where we stand already:
- 54,626 predictions made
- 5,081 predictions scored
- 9,504 points gained
Day one gave us an incident, a pile of feedback, and over 1,000 players. That's exactly what a launch should do. The tournament runs for over a month, and we'll keep shipping as fast as the feedback arrives. If you spot something off or wish a feature existed, tell us. Yesterday proved we'll probably build it by the next day.
