
Loading Games...

Loading Games...
The rankings page is the most visited page on this site. A huge chunk of new visitors land there first. It is the answer to the question the site was built to answer: what are the hardest games ever made, according to real players?
For a while, it was also the slowest page on the site. Sometimes embarrassingly so.
Every game on HDII has a difficulty score. That score is not just a simple average. It is a weighted score that factors in the number of ratings, the standard deviation of those ratings, and a confidence adjustment so that a game with three votes does not unfairly outrank a game with fifty.
The problem is that computing all of that, across every game in the database, is not free. The original implementation ran that aggregation query live on every page load. It was joining across ratings, computing averages and standard deviations per game, applying the weighting formula, and sorting the results. On a cold request, that could take several seconds.
The obvious fix is a cache. And there was one. An in-memory cache that would hold the computed results for an hour and skip the heavy query on repeat visits.
That cache worked fine in development.
HDII runs on Vercel. Vercel is serverless, which means there is no single long-running server process sitting in memory between requests. Each request can land on a different function instance. Each instance starts cold. Each instance has its own memory.
In practice, this means an in-memory cache is basically decorative. You might get lucky and hit a warm instance. More often, especially during traffic spikes when Vercel spins up new instances to handle the load, you are starting from scratch. Every. Single. Time.
So the rankings page, the most popular page on the site and the first thing many users ever see, was running a multi-second database query on most requests.
That needed to change.
The fix was to move the cache out of memory and into the database. I added a table called game_rankings_cache. Instead of computing scores live, a background job rebuilds that table periodically, writing the pre-computed weighted score, rating count, average, and standard deviation for every game. The rankings query now just reads from that table. No aggregation. No joins across ratings. Just a simple select and sort.
Since the data lives in the database rather than in a server process, every Vercel function instance reads the same pre-computed rows. The problem of cold starts evaporates. Whether Vercel routes the request to a warm instance or spins up a fresh one, the data is there.
On top of that, the rankings page uses Next.js ISR (Incremental Static Regeneration) with a 10-minute revalidation window. So for most visits, Vercel is not even calling the database at all. It is serving a pre-rendered static snapshot and regenerating it quietly in the background when the window expires.
The rankings page now loads fast. The database cache rebuilds every 20 minutes, so scores stay reasonably fresh without hammering the database. The ISR layer means the vast majority of requests never touch the database at all.
A new rating still takes up to 20 minutes to show up in the rankings. That is a deliberate tradeoff. I would rather have a fast, reliable rankings page than a live one that sometimes makes the site's most important page crawl.
The irony is that the solution was not particularly clever. Materializing expensive queries into a table is one of the oldest tricks in the book. But it is a good reminder that in-memory caching is only reliable when you actually control the process. On a serverless platform, you do not.
See the rankings →