DEV Community

Big Mazzy
Big Mazzy

Posted on Originally published at serverrental.store

Caching Strategies for Dynamic Websites on a Budget VPS

A page that normally loaded in 180 ms took nearly four seconds during a small traffic spike—not because the VPS ran out of CPU, but because every request repeated the same database work. A short cache at the web server would have absorbed most of that load. This walkthrough covers a practical caching stack for a budget VPS, including what to cache, how to avoid leaking personalized pages, and how to check that the cache is actually working.

Start with the cheapest wins

Caching is not one switch. It’s a few layers, each useful for different kinds of content:

  • Browser caching reduces repeat downloads of static files.
  • Reverse-proxy caching lets Nginx serve eligible responses without calling your application.
  • Application or data caching avoids repeating expensive database queries or computations.

Start with the layer that matches the bottleneck. If a page is slow because it runs a costly query, caching the CSS won’t fix it. If the page is fast but users repeatedly download a large bundle, browser caching is an easy win.

Before changing anything, get a baseline. Check a representative page a few times and note its response time, status code, and cache-related headers. Then check your application and database metrics if you have them. A cache that serves the wrong content is worse than a slow page.

Cache static assets in the browser

Give versioned assets—such as app.a1b2c3.js—a long browser lifetime. The filename changes when the content changes, so returning visitors can reuse the old download safely until they receive the new filename.

For Nginx, a typical rule looks like this:

location ~* \.(css|js|png|jpg|jpeg|gif|svg|webp|woff2)$ {
    expires 30d;
    add_header Cache-Control "public, max-age=2592000, immutable";
}
Enter fullscreen mode Exit fullscreen mode

Use immutable only if your build process changes the asset URL when its contents change. If you overwrite app.js in place, a 30-day cache can leave users running stale code. For HTML, use a shorter lifetime or revalidate; pages often need to reflect changes sooner than static assets.

Add a small Nginx microcache

For public pages that are expensive to render but can tolerate a few seconds of staleness, Nginx’s proxy cache is a good fit. A 5–15 second cache can collapse a burst of identical requests into one application request without making a page feel stale to most visitors.

The example below assumes Nginx proxies requests to an application listening on 127.0.0.1:3000. Put the map and proxy_cache_path directives in the http block, not inside a server block:

http {
    proxy_cache_path /var/cache/nginx
        levels=1:2
        keys_zone=site_cache:10m
        max_size=256m
        inactive=10m
        use_temp_path=off;

    map $http_cookie $skip_cookie_cache {
        default 1;
        ""      0;
    }

    map $http_authorization $skip_auth_cache {
        default 1;
        ""      0;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://127.0.0.1:3000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_cache site_cache;
            proxy_cache_key "$scheme$request_method$host$request_uri";
            proxy_cache_methods GET HEAD;
            proxy_cache_valid 200 10s;
            proxy_cache_valid 404 1s;
            proxy_cache_lock on;

            proxy_cache_bypass $skip_cookie_cache $skip_auth_cache;
            proxy_no_cache     $skip_cookie_cache $skip_auth_cache;

            add_header X-Cache $upstream_cache_status always;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This conservative setup bypasses caching whenever a request has a cookie or an Authorization header. That’s safer than trying to guess which cookies imply a logged-in user. If your site sets an anonymous analytics cookie on every visit, this rule may bypass more requests than necessary; refine it only after confirming which cookies affect page content.

Check the configuration and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

Then request a public page twice:

curl -sSI https://example.com/ | grep -iE 'HTTP/|x-cache|cache-control'
Enter fullscreen mode Exit fullscreen mode

The first request will often show MISS; a later request may show HIT. A response can still be uncacheable if the application sends headers such as Cache-Control: private or no-store. That’s usually intentional—don’t override those headers until you understand why they’re there.

proxy_cache_lock on is a useful, non-obvious safeguard: when a popular item expires, it helps prevent many simultaneous requests from all regenerating it at once. Also, keys_zone=site_cache:10m is Nginx’s in-memory index, not the maximum disk cache size; max_size=256m sets the disk limit.

Keep personalized and changing content out

A page containing a user’s name, cart, account balance, or private dashboard should not be served from a shared cache. Bypassing on cookies and authorization headers is a safe starting point, but the application should also send appropriate Cache-Control headers for private responses.

Be equally deliberate with query parameters. Nginx’s default cache key includes the request URI, including its query string. That’s often right for searches or filters, but it can create many nearly identical cache entries for tracking parameters such as utm_source. If you normalize URLs, confirm first that the removed parameters never change the response.

For content that changes immediately—inventory, account settings, or breaking news—use no cache or a very short lifetime. For articles and public landing pages, a few seconds may be enough to smooth traffic spikes while keeping updates reasonably fresh.

Use application caching for expensive work

A reverse proxy can cache whole responses; it can’t efficiently cache every expensive operation inside your application. If one database query dominates page generation, cache that result in the application using a key that includes every input that affects the answer.

For example, a product list might be cached by category and page number, with a short expiration. Avoid a single key such as products if different users, locales, currencies, or filters receive different results.

A Redis server can be useful for shared application data, but it adds another service to monitor and size. On a small VPS, start with a bounded cache and watch memory use. An unbounded in-process cache can quietly consume the RAM your database or application needs.

Fit the cache to the VPS

Disk cache is useful when RAM is limited, but it is not free: the cache competes with logs, uploads, and database files for storage. Start small—perhaps a 128–256 MB Nginx cache—and inspect disk usage before increasing it. For a single-site VPS, a short microcache and browser caching are often better starting points than adding multiple services.

When choosing a VPS, compare memory, disk type, bandwidth, and support as well as the monthly price. PowerVPS is one option worth comparing for a small web deployment. For a site that adds AI-generated features, Vast.ai can be useful for GPU rental, but a GPU usually won’t improve ordinary page caching; keep that workload separate unless you have a specific reason to colocate it. The Server Rental Guide is also a useful resource when comparing hosting and rental options.

Make changes safely

A practical rollout looks like this:

  1. Record baseline response times and identify one slow, public page.
  2. Add browser caching for versioned static assets.
  3. Enable a short reverse-proxy cache for eligible GET and HEAD responses.
  4. Confirm repeated requests show cache hits and personalized requests bypass the cache.
  5. Watch application load, disk usage, and error rates during real traffic.
  6. Adjust the cache lifetime only after checking freshness requirements.

One more tip from practice: test as both an anonymous visitor and a logged-in user. A page that looks correct in your own browser may be getting a cache hit from a previous anonymous request—or the reverse. Also test after a content update. A cache that works perfectly until an editor publishes an urgent correction still needs an invalidation plan; for a small site, a short TTL may be the simplest plan.

Summary

A budget VPS can handle more traffic without a complicated cache stack. Cache versioned static assets in browsers, use a short Nginx microcache for safe public pages, and cache expensive application data where it makes sense. Keep personalized responses out of shared caches, verify hits and bypasses with real requests, and size the cache against your actual disk and memory budget. Start small, measure the result, and expand only where the numbers point.

Top comments (0)