Why Your Website Serves Old Files: Debug Cache-Control and ETags with cURL

You deploy a new stylesheet, refresh your website, and still see the old design. A teammate gets the new version immediately. A customer sees something else. Is the browser caching a file? Is a CDN serving an old response? Or is the server sending different content than you expect?

Caching problems become much easier to investigate when you inspect the HTTP response rather than repeatedly pressing Refresh. Two headers, Cache-Control and ETag, explain a large part of what is happening. cURL can help you examine them without the extra behavior of a browser page.

You can prepare a request with Web2Generators’ cURL command line builder, then run and adapt the resulting command in your terminal. The builder creates commands locally; it does not execute requests for you.

First, understand the two different cache questions

HTTP caching has two separate decisions:

  • Can a stored response be reused without contacting the server? Freshness directives such as max-age help answer that question.
  • If the stored response needs checking, has the resource changed? Validators such as ETag or Last-Modified help answer that one.

This distinction explains why a browser can display a cached page even when no new network response appears, or make a network request yet avoid downloading the entire file.

The MDN guide to HTTP caching describes the wider mechanism and its tradeoffs.

Step 1: Fetch the response headers

Start with a non-sensitive page or asset on a site you are authorized to test. To see the response headers along with the body, use --include:

curl --include --url 'https://example.com/assets/site.css'

example.com is illustrative here. Replace it with a real URL for your own site. Depending on the server, a successful response might include:

HTTP/2 200
content-type: text/css
cache-control: public, max-age=3600
etag: "build-42"

These illustrative headers describe a one-hour freshness lifetime and a validator named "build-42". The hour is based on the response’s age, not an extra hour each time a browser receives it.

The Web2Generators builder supports request methods, custom headers, and verbose output. For an initial GET, enter the URL, leave TLS verification enabled, and copy the command. You can add --include manually afterward, or use the builder’s verbose option to inspect request and response details. Read the existing cURL command guide for more options and shell-quoting tips.

Step 2: Interpret common Cache-Control directives

These directives are related, but they are not interchangeable:

DirectivePractical meaning
max-age=3600A response can be considered fresh for up to 3,600 seconds under normal HTTP cache rules.
no-cacheStorage is allowed, but the stored response must be revalidated before reuse.
no-storeA cache is instructed not to store the response.
privateIntended for a private cache, not a shared intermediary cache.
publicExplicitly permits storage by shared caches when other requirements are satisfied.

The often-confusing one is no-cache: it does not mean “never store this.” It means do not reuse a stored response without checking with the origin or an appropriate validating cache.

Also inspect Age, Expires, and any documented CDN-specific headers if present. For example, Age: 3500 leaves roughly 100 seconds of a max-age=3600 lifetime, before accounting for transit time. An intermediary can influence which response reaches the browser. One header alone does not tell the whole story.

Step 3: Revalidate with an ETag

An ETag is a server-provided identifier for a representation of a resource. Once you have a real tag from a previous response, send it back in If-None-Match:

curl --include \
  --url 'https://example.com/assets/site.css' \
  --header 'If-None-Match: "build-42"'

If the selected representation still matches and the request conditions are satisfied, the server can return:

HTTP/2 304
etag: "build-42"
cache-control: public, max-age=3600

A 304 Not Modified response does not contain a new representation body. It tells a client with the corresponding cached response that it can use that stored copy. If the server’s current representation is different, you would ordinarily receive a 200 response with the new content.

Do not expect a 304 just because you copied this example. You need a resource that actually sends ETags, a matching validator, and a compliant response path. Some servers use Last-Modified and If-Modified-Since instead. MDN’s conditional requests guide explains both approaches.

Step 4: Check whether the cache is serving the right variant

A response can vary by more than its URL. Common dimensions include compression and language. For example, a server that selects a response based on the Accept-Language request header may need an appropriate Vary: Accept-Language response header for caches that store those variants.

If your site recently added another language, test both localized URLs and any language-negotiated responses.

You can use the cURL builder to add a custom header such as:

Accept-Language: fr

Then compare results with an English request if the server is designed to negotiate language this way. Many multilingual websites instead serve separate language URLs, and a language header may have no effect. Do not assume a header changes content merely because your client sent it.

Step 5: Look beyond the origin server

If cURL receives current CSS while the browser renders old CSS, inspect the exact URL loaded in the browser. A page may reference an older filename, a service worker may return a cached asset, or a CDN may still be serving a cached response to a particular route.

Open the Network panel, reload, and compare the request URL and response headers. Web2Generators’ guide to checking browser network requests explains the process in Chrome, Firefox, Edge, and Safari.

A browser’s “Disable cache” option, when available, helps isolate browser HTTP cache behavior while developer tools are open. It does not necessarily bypass a CDN, service worker, application cache, or server-side cache. Use it as one test, not a verdict.

Step 6: Choose caching policies by content type

Different resources need different tradeoffs:

Versioned static files. A CSS file such as app.7f31a2.css, whose name changes whenever its contents change, can often have a long freshness lifetime. Make sure deployment updates the HTML references to the new filename. Content-addressed naming avoids asking caches to guess whether an unchanged URL contains new content.

HTML documents. These often need a shorter freshness lifetime or revalidation so visitors discover newly deployed asset references. The right policy depends on the site and hosting setup.

Personal or sensitive responses. Never assume an API response is safe for shared caches simply because it is fast to fetch. Authentication, user-specific representations, cookies, and confidentiality require deliberate cache design. Use appropriate controls, and avoid putting tokens or personal data in copied diagnostic commands or logs.

Remember that caching is a product decision as well as a performance decision. A price, account page, or security notification has different freshness needs from an immutable icon.

A five-minute diagnostic sequence

When someone reports an old page, gather the exact URL and approximate time. Fetch its headers with cURL. Record the status, Cache-Control, validator, and any intermediary indicators. Check whether the browser requested the same URL. Then make a conditional request using a real ETag when available.

If the browser and terminal disagree, investigate the layers between them rather than assuming that “the cache” is one thing. If both cURL and the browser return an old version, check the deployment and any shared cache in front of it. A cURL request to the public URL does not prove you reached the origin directly.

Common questions

Is a 304 status an error?

No. For a conditional GET or HEAD, 304 Not Modified is normally a successful validation signal telling the client to reuse its existing representation. It is not a server failure.

Will adding a query string always fix stale CSS?

Changing the URL can create a distinct cache key, but random query strings are a diagnostic workaround, not a complete release strategy. Versioned filenames and coherent response headers are usually easier to maintain.

Does the cURL builder bypass caches?

No. It constructs HTTP commands. When you run one, your request can still pass through reverse proxies, CDNs, and other intermediaries. Inspect the returned headers and the actual destination.

Test the URL that is serving old content

Open the free cURL command builder, enter the exact URL from your browser’s Network panel, and enable Verbose output under Advanced options. Copy the command into your terminal and compare the response headers. If the response includes an ETag, add it as an If-None-Match custom header for a second request. You now have a repeatable check to run after your next deployment.

Premium is coming soon

An optional premium subscription to remove ads is planned. Subscriptions are not available yet.

You can keep using our free tools without an account.