Running a site with 56,000 pages sounds like it needs serious infrastructure. In our case it runs on a single virtual machine. GameZiko serves tens of thousands of individual game pages, and the only reason one small box survives is a caching setup that treats nginx as the real server and the backend as a page factory that only runs when it has to.
Here is the mental model. A game page barely changes. The game title, description, thumbnail, embed URL, and category do not move once published. The only dynamic parts are things like view counters and related-games lists. So the backend should render each page exactly once and never again until the content changes. Everything after that is nginx serving a cached file from memory or disk.
The core of the setup is proxy_cache (or fastcgi_cache if you run PHP-FPM; same idea). We define a cache zone sized generously, something like 10g on disk, and set long cache validity for page content: 24 hours for game pages, shorter for listing pages. The cache key includes scheme, host, and request URI. That is it. No cookies in the key, because anonymous traffic is 99 percent of the game.
Stale-while-revalidate is the trick that makes this feel instant. When a cached page expires, nginx serves the stale copy while the backend regenerates it in the background. The visitor never waits on a cold render. This matters because a full page render with database queries takes a few hundred milliseconds, and a cold cache during a traffic spike turns those milliseconds into a pileup. With background updates, the worst case a user ever sees is a slightly older page.
Microcaching handles the pages that are not quite static. Category listings and the homepage change often enough that a 24-hour cache would look broken, but they are also the pages that get hit the hardest. A 60-second microcache on those endpoints cuts backend hits by an order of magnitude without anyone noticing. Nobody refreshes a category page twice in a minute and complains.
Static assets get their own aggressive treatment. Thumbnails, CSS, and JS go behind expires 1y with content-hashed filenames, so browsers and CDNs hold them basically forever. The embed iframes themselves are served from the game files' origin, which means our VM never touches the heaviest bytes on the site. That separation is probably the single biggest architectural decision: HTML is ours, game assets are not, and nginx only caches what is cheap to store.
Compression is boring but mandatory. Brotli at a moderate level for HTML, CSS, and JS shaves a surprising amount off page weight. On a catalog site where every page shares the same template, compression ratios are excellent because the repeated structure compresses so well.
Rate limiting protects the cache from being poisoned by abuse. We limit aggressively on the search endpoint and the API-ish routes, because a bot hammering uncached query strings is the fastest way to melt a small server. Cached pages are cheap to serve, but only if the requests match cached keys. Anything that bypasses the cache is the enemy, so query strings get normalized and junk parameters get dropped before they reach the cache key.
Cache invalidation is deliberately simple. When a game page is updated or a new one is published, we purge its specific URL and the affected listing pages. There is no wildcard invalidation, no event bus, no distributed cache to coordinate, because there is one server and one cache. Simple invalidation you can reason about beats clever invalidation that surprises you at 2am.
Monitoring closes the loop. We watch the cache hit ratio per location block, and anything below about 95 percent on game pages gets investigated. A dropping hit ratio usually means a bot found a new uncached route or a template change broke the cache key. It is a cheap metric that catches expensive problems early.
None of this is exotic. It is nginx doing what nginx is good at: serving bytes fast and shielding a modest backend from the internet. The lesson for anyone running a large catalog on small hardware is that you do not need more servers first. You need fewer backend executions, and a cache layer in front that makes each execution count.
If you want to see what the other side of this stack looks like, the live catalog is at https://gameziko.com, where every one of those 56,000 pages is served through exactly this setup.












