When a website or API returns an older response than expected, the cause is often a caching decision made somewhere between the origin server and the client. This guide explains HTTP caching headers, shows how Cache-Control, ETag, and Last-Modified work together, and provides a repeatable checklist for finding stale content without disabling caching unnecessarily.
Overview
HTTP caching can happen in a browser, a shared proxy, a content delivery network (CDN), a framework runtime, or an application-specific cache. Each layer may interpret response headers and apply its own rules. As a result, changing a file or database record at the origin does not always mean that every request immediately receives the new value.
The most important header is usually Cache-Control. It tells caches whether a response may be stored and how it should be reused. Validators such as ETag and Last-Modified allow a client to ask whether a stored response is still current. If the representation has not changed, the server can return 304 Not Modified instead of sending the full response again.
A useful mental model is to separate three questions:
- May this response be stored? Review
public,private, andno-store. - How long may it be reused without checking? Review directives such as
max-ageands-maxage. - How can a cache check whether it is still valid? Review
ETagandLast-Modified.
For API work, caching policy should match the data. Public, versioned resources are usually easier to cache safely than user-specific responses, rapidly changing records, or responses containing confidential information. For frontend assets, fingerprinted filenames such as app.8f31c.js make long-lived caching more practical because a new filename represents a new version.
Checklist by scenario
Scenario 1: A browser shows an old CSS or JavaScript file
- Open the browser’s developer tools and inspect the actual asset request, not only the document request.
- Record the response status, especially whether it is
200or304. - Check the response’s
Cache-Control,ETag,Last-Modified, andAgeheaders when available. - Compare the requested URL with the deployed asset URL. A stale reference in the HTML can point to an older filename even when the server is configured correctly.
- Confirm that the build process changes asset names or query-string versions when content changes.
- Use a temporary reload strategy only for diagnosis. Do not treat disabling the browser cache as a production fix.
A common durable pattern is to give immutable, content-hashed assets a long freshness period while keeping the HTML document revalidated more frequently. The HTML then points to the current asset names after deployment.
Scenario 2: A CDN or reverse proxy serves stale content
- Send a request that includes response headers and identify whether the response came from the origin or an intermediary.
- Look for CDN-specific diagnostic headers, but treat their names and meanings as provider-specific.
- Compare responses from more than one location if the behavior appears inconsistent.
- Check whether
s-maxageor a CDN configuration overrides the browser-facingmax-age. - Confirm that the deployment process purges or revalidates affected paths when content is replaced.
- Prefer versioned URLs for assets that must change predictably rather than relying on broad cache purges for every release.
Remember that a cache purge affects a specific cache layer and set of paths. It may not clear a browser cache, an application cache, or another proxy. Document which layer is responsible for invalidation and include that step in the release procedure.
Scenario 3: An API returns an older record
- Define whether the endpoint is public, user-specific, or administrative.
- Check whether requests include cookies or authorization headers and whether the response varies by identity.
- Inspect
Cache-Controlbefore changing application code. A shared cache must not reuse a response across users when the representation is personalized. - Check for an application, database, or object cache in addition to HTTP caching.
- Use a validator when conditional retrieval is useful. The client can send
If-None-Matchfor an ETag orIf-Modified-Sincefor a modification date. - Test the update path and the read path separately. The write may succeed while a read cache still contains the previous representation.
For endpoints that must reflect a change immediately, consider a short freshness period, explicit invalidation after a successful write, or a no-store policy where appropriate. Avoid selecting a policy solely because it makes a single debugging test pass.
Scenario 4: You need a cache policy for a new endpoint
Start by classifying the response rather than copying a header from another route:
| Response type | Questions to answer | Typical direction |
|---|---|---|
| Public, stable data | Can different users receive the same representation? | Allow shared caching with a deliberate freshness period and validators. |
| User-specific data | Could another user see this response from a shared cache? | Use private caching or prevent storage when sensitivity requires it. |
| Frequently changing data | How much staleness is acceptable? | Use shorter freshness, revalidation, or explicit invalidation. |
| Versioned static asset | Does the URL change when the content changes? | Long-lived caching can be considered when filenames are reliably versioned. |
Record the decision alongside the endpoint documentation. A clear policy helps developers test the intended behavior instead of guessing from a single response.
What to double-check
Cache-Control directives
max-age describes how long a response may be considered fresh by a browser. s-maxage is intended for shared caches and may take precedence there. public permits shared storage, while private limits storage to a private cache. no-store instructs caches not to store the response. no-cache is commonly misunderstood: it does not necessarily mean “never store”; it indicates that stored content must be revalidated before reuse.
Also check must-revalidate when serving stale content is not acceptable after freshness expires. The exact result still depends on the cache and request conditions, so validate behavior with real headers and requests rather than relying on the directive name alone.
ETag versus Last-Modified
An ETag is an identifier for a particular representation. A client can return it in If-None-Match, allowing the server to decide whether the representation is unchanged. A Last-Modified value gives a time-based indication and is used with If-Modified-Since. ETags can distinguish changes that a timestamp may not capture, while modification dates are straightforward for systems that already track reliable update times.
Do not assume that receiving a 304 means the origin generated a fresh body. It means the validator matched the request conditions and the cached representation may be reused. Inspect the complete request chain when the cached representation itself is wrong.
Request variation and security
Review the Vary header when the response changes based on request headers such as Accept-Encoding or Accept-Language. Incorrect variation can produce unexpected content or reduce cache effectiveness. Be especially careful with cookies, authorization, tenant identifiers, and other values that distinguish users. A response that includes private information should not be treated as a generic shared response.
When debugging API behavior, pair this checklist with a broader API development checklist. If the visible symptom is a browser failure rather than stale data, inspect the request separately with the CORS errors guide.
Common mistakes
- Using a query string as an unplanned cache purge: Versioned URLs can work well, but they should be generated consistently and supported by deployment tooling.
- Setting
no-cachewhen the goal is no storage: Chooseno-storewhen a response must not be stored, and verify that the policy fits the data. - Only testing from one layer: A browser, CDN, reverse proxy, framework, and database cache may all produce different evidence. Test the origin directly when possible, then compare each intermediary.
- Changing headers without checking the request: Cookies, authorization, conditional headers, and request methods can alter caching behavior.
- Assuming deployment completion equals cache invalidation: Include cache invalidation or asset versioning in the release checklist.
- Disabling all caching to solve one stale response: This may hide the root cause and remove useful performance benefits. Identify the stale layer and correct its policy.
For frontend symptoms such as an old bundle, unexpected stylesheet, or misleading browser result, the frontend debugging checklist can help separate caching from JavaScript and CSS problems.
When to revisit
Review caching headers whenever an endpoint changes its data sensitivity, response shape, authentication model, or update frequency. Revisit asset caching when the build pipeline, filename strategy, CDN configuration, or deployment process changes. A platform migration is also a good time to verify whether the new proxy or hosting layer interprets the existing headers as expected.
Before a significant release, run this practical sequence:
- List the responses that should be cached, revalidated, or kept private.
- Inspect representative responses from the origin and from the public delivery path.
- Test a first request, a repeat request, an updated resource, and a request with relevant user context.
- Confirm that a changed asset or record becomes visible according to the documented expectation.
- Write down the invalidation step, rollback behavior, and the headers that future debugging should inspect.
Keep this checklist near deployment and API documentation, then update it when workflows or tools change. If stale content appears again, begin with the response headers and the cache layer that served the response. That evidence usually provides a faster path to a precise fix than repeatedly refreshing the page or clearing every cache.