The Cache-Control header sets rules for storing and reusing HTTP responses. Before launching a website, choose a policy for each response type: HTML that discovers the current release, assets whose URLs change with their content, and private responses that must not enter a shared cache. One blanket policy can leave visitors on old pages or make every repeat visit download unchanged files.
This guide covers delivered response headers and stale deployment troubleshooting for public websites, portfolios, and student projects. Start with the production URL: a correct application setting can be replaced by hosting middleware or a CDN rule before it reaches the browser.
Cache-Control header launch checklist
- Inventory the HTML, asset, and API routes that need different policies.
- Require revalidation for release-sensitive HTML, or document an acceptable freshness window.
- Use long asset lifetimes only when changing content produces a new URL.
- Keep personalized responses out of shared caches.
- Inspect the final GET response, including the body and its asset references.
- Test a returning browser as well as a clean session after deployment.
- Verify conditional responses, old asset availability, and rollback behavior.
- Add delivered-header checks to CI and record the release being tested.
Keep this pass alongside the broader website launch checklist. Cache behavior affects whether the page you tested is the page a returning visitor receives.
Understand no-cache vs no-store
These directives solve different problems:
no-cachepermits HTTP storage but requires successful validation before reuse.no-storeinstructs HTTP caches not to store the response.privateprevents a shared cache from storing the response; a browser cache may still store it.max-age=600defines a ten-minute freshness lifetime, with response age taken into account.s-maxage=600sets the freshness lifetime for shared caches, overridingmax-agethere.
RFC 9111's response directives define these distinctions. In particular, no-cache does not mean “never save this response,” and private does not encrypt content or replace authentication. Choose a rule from the route's data and update requirements.
For example, a public documentation page may benefit from stored HTML that is validated on each new fetch. A response containing a user's account details may require a no-storage policy. Do not apply a shared public cache rule to both simply because they use the same framework.
Choose separate policies for HTML and assets
The following is a starting contract for a static public site with content-fingerprinted assets, not a universal hosting configuration:
# Public HTML whose release should be checked before reuse
Cache-Control: no-cache
# Asset whose URL changes whenever its content changes
Cache-Control: public, max-age=31536000, immutable
# Sensitive account or session response
Cache-Control: no-store
The comments separate three responses; do not combine all three header values on one response. Set these rules in the response layer that owns each route, then verify what production sends. Paths such as /docs, /assets, and /api are examples: your application's routes determine the actual matching rules.
Revalidate HTML with a useful validator
HTML often points to the current JavaScript and CSS filenames. Letting that document stay fresh for a year can keep a visitor on an old release even when the new bundles are already uploaded.
With no-cache, a stored document can be checked using an ETag or Last-Modified validator. A matching conditional request can receive 304 Not Modified, allowing reuse of the stored body. The MDN ETag reference explains this exchange. Ensure a content change also changes its validator; an incorrectly stable ETag can keep stale HTML looking valid.
If a public page intentionally tolerates five minutes of old content, a finite freshness policy may be appropriate instead. Write down that delay and include any stale-serving extension in it. A release announcement, rapidly changing price, and static tutorial may need different decisions.
Cache immutable assets only under versioned URLs
A filename such as /assets/app.8d73ac.js can identify one exact build artifact. The next build must receive another URL when its bytes change. The immutable directive tells supporting clients that a fresh response will not change during its freshness lifetime; RFC 8246 defines its behavior.
Do not serve changing content at /app.js with a year-long immutable policy. Renaming a file once is not enough: the build and deployment process must maintain the content-to-URL contract for every release.
Keep previous assets available for the transition and rollback period. A returning visitor may still have old HTML, and an already open tab may request an old lazy-loaded chunk later. Upload new assets before switching HTML; remove old assets only according to a reviewed retention policy. If a missing chunk causes a runtime failure, use the JavaScript console errors guide to capture the request and initiating code.
Inspect the delivered Cache-Control header
Use a GET request so you inspect the same response type as an ordinary navigation:
curl --silent --show-error --location --max-redirs 5 \
--dump-header /tmp/launch-headers.txt \
--output /tmp/launch-page.html \
https://example.com/docs/getting-started
cat /tmp/launch-headers.txt
Replace the example host and path with a route you own. Redirects create multiple header blocks: review the final document response, then request one actual script or stylesheet URL from the saved HTML. The HTML and asset should follow their separate policies.
Record the status, final URL, Cache-Control, ETag, Last-Modified, Age, and any hosting-specific cache-status header that is present. Also record a release marker or exact asset filename from the body. A correct header on the wrong release is still a deployment problem.
Age provides information about the response's age in caching infrastructure. A CDN can also impose its own rules, so inspect both its configuration and delivered response. MDN's HTTP caching guide distinguishes browser, shared, and managed caches and explains why their behavior needs separate consideration.
Avoid treating a random query parameter as proof that you reached the origin. A provider may ignore, include, or normalize it in its cache key. Use the provider's documented inspection or bypass mechanism when isolating the edge layer.
Find why an old deployment still appears
Work from evidence rather than asking every user to clear their browser:
- Compare the public GET body with the intended build. If it is old, inspect the deployment target and CDN document rule.
- Open Chrome DevTools Network, load the route, and inspect the document and asset requests with caching enabled.
- Reload with Disable cache enabled while DevTools is open, then compare the response and filenames.
- If a service worker controls the page, inspect its registrations and Cache Storage separately.
- Test a browser that visited the previous release, including a late navigation that loads another chunk.
Chrome's Network panel reference documents the cache switch and response inspection. Its service worker debugging guide describes Bypass for network, which helps isolate service worker interception. Disabling the ordinary browser cache does not purge a CDN.
If a clean browser gets the new page while an existing profile gets the old one, compare the controlling service worker, stored responses, and HTML policy. If both get old HTML, investigate the edge and deployment before changing client code. If both get new HTML but a script returns 404, inspect asset publication and retention.
Purging a CDN can replace its stored response, but it cannot remotely delete every visitor's already fresh browser copy. Fix the policy and asset URL strategy so the next release does not depend on emergency cache clearing. HTTP directives also do not guarantee that an already open page will refresh itself; test that user journey separately.
Add cache header testing to CI
For a site that deliberately adopts the three policies above, a small Playwright request test can catch hosting-rule drift. This example assumes a Playwright Test project and a public preview or deployment you control:
import { test, expect } from "@playwright/test";
const origin = process.env.RELEASE_ORIGIN;
const assetPath = process.env.RELEASE_ASSET_PATH;
test("HTML revalidates and the release asset is immutable", async ({ request }) => {
if (!origin || !assetPath) {
throw new Error("Set RELEASE_ORIGIN and RELEASE_ASSET_PATH from this build");
}
const base = new URL(origin);
const assetUrl = new URL(assetPath, base);
expect(assetUrl.origin).toBe(base.origin);
const html = await request.get(new URL("/docs/getting-started", base).href);
expect(html.status()).toBe(200);
expect(html.headers()["content-type"]).toContain("text/html");
expect(html.headers()["cache-control"]).toBe("no-cache");
expect(await html.text()).toContain(assetPath);
const asset = await request.get(assetUrl.href);
expect(asset.status()).toBe(200);
expect(asset.headers()["content-type"]).not.toContain("text/html");
const directives = (asset.headers()["cache-control"] ?? "")
.toLowerCase().split(",").map((value) => value.trim());
expect(directives).toEqual(expect.arrayContaining([
"public", "max-age=31536000", "immutable",
]));
expect(directives).not.toContain("no-store");
expect(directives).not.toContain("no-cache");
expect(directives).not.toContain("private");
});
Change the document route and expected headers to match your reviewed contract. Supply the real asset path from the build manifest, rather than a hardcoded filename from a previous release. This assertion assumes that exact path appears in the HTML; adapt it if your build uses different URL serialization or indirect asset discovery.
Playwright's APIRequestContext reference documents GET requests and redirect handling. These requests check delivered headers and content, not browser cache reuse or service worker behavior. Keep the returning-browser test above, and add an explicit conditional GET test when validators are part of your contract.
For performance comparisons, record cold and warm cache conditions separately. The website performance budget guide explains how cache state changes transfer measurements. Save header results with the build revision using the website regression testing workflow.
Verify the public release with Slop Audit
After the cache policy and returning-browser checks pass, run Slop Audit on the deployed public URL. Review complementary evidence: final response status and URL, title and description, canonical metadata, bounded same-origin link probes, and available browser evidence for runtime errors and layout. This gives you another record of the page observed after deployment.
Slop Audit does not currently evaluate Cache-Control directives, validate ETags, or reproduce a returning visitor's cache history. Keep those checks in your release tests. Use the audit's findings and coverage ledger to investigate the public page it could observe; a readiness score does not prove every visitor received the latest build.
Before signing off, record the document policy, one current asset URL, one retained previous asset URL, the release identifier, and the returning-browser result. That short record makes a stale-page report actionable without guessing which cache served it.