Cache before server rendering: reduce latency and CPU without a full static build

HTTP cachingserver renderingperformance

A practical rollout plan for teams that want to cache public SSR or edge-rendered responses, avoid caching private data, and verify hit, miss, freshness, and purge behavior before release

Scenario: optimize repetition, not the framework

A team has server-rendered pages: a catalog, documentation, marketing pages, public profiles, or articles. Some responses are identical for thousands of users, but the application still runs code on every request: reads data, builds HTML, executes templates, and returns the response. Latency grows, CPU is wasted, and the team starts considering a full static build.

Sometimes the better first step is simpler: put a cache in front of the code. If a ready public response already exists and is safe to reuse, serve it from the cache. If it does not exist, the request reaches the origin, the code runs once, and the result is stored for later requests.

A current example of this model is Cloudflare Workers Cache, where a cache can sit in front of a Worker that is itself the origin for a server-rendered application. But the main lesson is not the specific product. The lesson is the pattern: the cache should stand before expensive code execution, not only before static files.

Step 1. Inventory routes before touching headers

Do not start with Cache-Control. Start with a route table. For every route, write down who sees the response, what it depends on, and how quickly it must update.

A practical split:

  • public: the page is the same for everyone, such as documentation or a public article;
  • conditionally public: the response depends on language, country, currency, or device type;
  • private: the response depends on the user, authentication, cookies, role, or session.

Private responses must not enter a shared cache. If a page contains a user name, personal orders, subscription status, or other personal data, use Cache-Control: no-store and verify that platform settings cannot make the response cacheable by accident.

Step 2. Headers: Cache-Control and Vary as a contract

For a public route, the header can be short and clear, for example Cache-Control: public, max-age=60, stale-while-revalidate=300. It means: the response may be stored, it is fresh for 60 seconds, and after that the cache may still serve the old version quickly while refreshing it in the background.

But Cache-Control is only half of the contract. The other half is Vary. If the page has different language versions, the cache must know that Accept-Language changes the response. If the response is compressed differently, Accept-Encoding is often part of the variation. If you are tempted to vary by Cookie, stop and review the route: it may actually be private and unsuitable for a shared cache.

Check Set-Cookie separately. A response that sets a cookie often should not be cached as public. A common mistake is a public page that adds a tracking or session cookie, while the team tries to cache it without reviewing response headers.

Step 3. stale-while-revalidate: speed with boundaries

stale-while-revalidate is useful for pages where a small update delay is acceptable. Documentation, blog posts, help pages, or a catalog without critical stock data may tolerate a few minutes of staleness. The user receives a fast response, while the cache refreshes in the background.

It is a poor choice for data where an old response causes harm: checkout prices, access status, balances, personal messages, or legal restrictions. For those routes, either avoid caching or use very narrow rules with explicit purge after data changes.

Agree on a freshness SLA: how many seconds or minutes a page may be stale without becoming an incident. If the team cannot answer, the stale-while-revalidate value is not ready yet.

Step 4. Purge strategy: not a panic button, a content map

A cache without a purge strategy quickly becomes a source of fear. You need a map: which content change removes which responses.

Examples:

  • publishing an article purges its path and the article list;
  • changing a product purges the product page, category, and search fragments;
  • updating navigation purges all pages with that menu or the related Cache-Tag;
  • changing a translation purges only the language version, not the entire site.

Cloudflare mentions Cache-Tag and programmatic purge as part of the Workers Cache model. On any platform, think the same way: purge by tag or prefix should be precise enough. An anti-pattern is purging the whole cache after every small change. That sends load back to the origin and hides problems in the dependency map.

Step 5. Smoke tests in staging

Before production rollout, run a small verification set.

  1. Open a public route twice. The first request should show cache miss, the second should show cache hit.
  2. Check logs or CPU metrics: during cache hit, the application should not perform full response generation.
  3. Repeat the test for different languages or other Vary values. Versions must not mix.
  4. Inspect response headers: no Set-Cookie, no private data, no dependency on Authorization.
  5. Update content, purge by path or tag, and confirm that the next request sees the new version.
  6. Temporarily disable the cache or route rule and verify that requests correctly fall back to the origin.

Anti-patterns to catch before release

The first anti-pattern is caching everything that is slow. Slowness does not make a response safe to reuse.

The second is ignoring Vary. If a page depends on language but the cache does not know it, users may receive the wrong language version.

The third is using a long max-age without purge. That creates a fast site that nobody trusts.

The fourth is testing only speed. For SSR and edge responses, you must also test privacy, freshness, and whether cache hit really avoids expensive code execution.

Careful rollout

Start with one low-risk public route: documentation, a help page, or an article. Add headers, Vary, purge behavior, and smoke tests. Then expand coverage to route groups that share the same rules.

The goal is not to turn the whole application into a static site. The goal is to remove unnecessary repetition where the response is already ready, public, and updated under control.

Sources

Quick checklist

  • list routes and mark public, private, and conditionally public responses
  • block caching for responses with authentication, personal data, or Set-Cookie
  • set Cache-Control and Vary for every cacheable route
  • decide where stale-while-revalidate is safe and how much staleness is acceptable
  • prepare purge by path, prefix, or tag for content update events
  • verify cache hit, cache miss, freshness, and absence of private data in staging

Build a safe caching plan for SSR or edge routes

You are helping prepare a careful rollout of a cache in front of a server-rendered application. Ask clarifying questions when data is missing. Request these inputs: - route list and example responses; - which routes depend on authentication, cookies, language, country, or device; - acceptable content update delay for each page type; - how content changes in the CMS, database, or admin tool; - which cache hit and cache miss signals are available on the platform; - how the team can purge by path, prefix, or tag; - staging and production rollout constraints. Return the result in this format: 1. Route matrix: route, public/private, cacheable, reason, risk. 2. Recommended headers: Cache-Control, Vary, Set-Cookie policy. 3. stale-while-revalidate plan: values, when appropriate, when forbidden. 4. Purge map: content change event, path or tag, expected effect. 5. Smoke tests: commands or steps for hit/miss, freshness, privacy. 6. Rollback plan: how to disable the cache and verify fallback to the origin. Do not recommend caching responses with personal data, authentication, or Set-Cookie without a separate justification.