惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
GbyAI
GbyAI
博客园 - Franky
宝玉的分享
宝玉的分享
Blog — PlanetScale
Blog — PlanetScale
Google DeepMind News
Google DeepMind News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
人人都是产品经理
人人都是产品经理
Microsoft Azure Blog
Microsoft Azure Blog
The Register - Security
The Register - Security
腾讯CDC
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
I
InfoQ
The Cloudflare Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
有赞技术团队
有赞技术团队
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
F
Full Disclosure
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Vercel News
Vercel News
博客园 - 【当耐特】
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Schneier on Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Project Zero
Project Zero
量子位
M
MIT News - Artificial intelligence
Stack Overflow Blog
Stack Overflow Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
美团技术团队
Attack and Defense Labs
Attack and Defense Labs
C
Cybersecurity and Infrastructure Security Agency CISA
T
The Blog of Author Tim Ferriss
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Troy Hunt's Blog
Google Online Security Blog
Google Online Security Blog
罗磊的独立博客
P
Proofpoint News Feed
Schneier on Security
Schneier on Security
Spread Privacy
Spread Privacy
S
SegmentFault 最新的问题
L
LINUX DO - 最新话题
Simon Willison's Weblog
Simon Willison's Weblog
爱范儿
爱范儿
博客园 - 聂微东
A
About on SuperTechFans
PCI Perspectives
PCI Perspectives
D
Docker

CSS Wizardry

Front-End’s Missing Metric: The TBT Window Meet Your Users Where They Are with Obs.js Better Browser Caching with No-Vary-Search font-family Doesn’t Fall Back the Way You Think What Is CSS Containment and How Can I Use It? When All You Can Do Is All or Nothing, Do Nothing Obs.js: Context-Aware Web Performance for Everyone Low- and Mid-Tier Mobile for the Real World (2025) The Fastest Site in the Tour de France Making Sense of the Performance Extensibility API HTML Is Not a Programming Language… Build for the Web, Build on the Web, Build with the Web Licensing Code on CSS Wizardry A Layered Approach to Speculation Rules Designing (and Evolving) a New Web Performance Score Core Web Vitals Colours The Ultimate Contract Templates for Tech Consultants: Protect Your Business and Get Paid Optimising for High Latency Environments Cache Grab: How Much Are You Leaving on the Table? blocking=render: Why would you do that?! Correctly Configure (Pre) Connections The Three Cs: 🤝 Concatenate, 🗜️ Compress, 🗳️ Cache What Is the Maximum max-age? How to Clear Cache and Cookies on a Customer’s Device The Ultimate Low-Quality Image Placeholder Technique Core Web Vitals for Search Engine Optimisation: What Do We Need to Know? The HTTP/1-liness of HTTP/2 In Defence of DOM­Content­Loaded Site-Speed Topography Remapped Why Not document.write()? Speeding Up Async Snippets Critical CSS? Not So Fast! Measure What You Impact, Not What You Influence Optimising Largest Contentful Paint Measuring Web Performance in Mobile Safari Site-Speed Topography Speed Up Google Fonts Real-World Effectiveness of Brotli Performance Budgets, Pragmatically Lazy Pre-Browsing with Prefetch Making Cloud.typography Fast(er) Time to First Byte: What It Is and How to Improve It Self-Host Your Static Assets Tips for Technical Interviews Cache-Control for Civilians Bandwidth or Latency: When to Optimise for Which ITCSS × Skillshare What If? CSS and Network Performance The Three Types of Performance Testing Getting to Know a Legacy Codebase Image Inconsistencies: How and When Browsers Download Images Identifying, Auditing, and Discussing Third Parties My Digital Music Setup Measuring the Hard-to-Measure Finding Dead CSS The Fallacies of Distributed Computing (Applied to Front-End Performance) Ten Years Old Relative Requirements Airplanes and Ashtrays Performance and Resilience: Stress-Testing Third Parties Refactoring Tunnels Little Things I Like to Do with Git Writing Tidy Code Configuring Git and Vim Base64 Encoding & Performance, Part 2: Gathering Data Base64 Encoding & Performance, Part 1: What’s Up with Base64? Code Smells in CSS Revisited Typography for Developers Moving CSS Wizardry onto HTTPS and HTTP/2 Ack for CSS Developers A New Year, a New Focus Preparing Vim for Apple’s Touch Bar Choosing the Correct Average CSS Shorthand Syntax Considered an Anti-Pattern CSS Wizardry Newsletter Nesting Your BEM? Improving Perceived Performance with Multiple Background Images Continue Normalising Your CSS Pure CSS Content Filter Pragmatic, Practical, and Progressive Theming with Custom Properties Refactoring CSS: The Three I’s Speaker’s Checklist: Before and After Your Talk Improving Your CSS with Parker The Importance of !important: Forcing Immutability in CSS Mixins Better for Performance Managing Typography on Large Apps White October Events Workshop Partnership BEMIT: Taking the BEM Naming Convention a Step Further Travelling Like You Want to, When You Have To Contextual Styling: UI Components, Nesting, and Implementation Detail Subtleties with Self-Chained Classes Cyclomatic Complexity: Logic in CSS Immutable CSS Can CSS Be Too Modular? More Transparent UI Code with Namespaces When to use @extend; when to use a mixin The Specificity Graph CSS Wizardry Ltd.: Year 1 in review CSS Guidelines 2.0.0
Why Do We Have a Cache-Control Request Header?
Harry Roberts · 2025-03-07 · via CSS Wizardry

(last updated on )

Written by on CSS Wizardry.

Table of Contents

Independent writing is brought to you via my wonderful Supporters.

  1. Cache-Control Recap
  2. Cache-Control as a Request Header
  3. Refresh
  4. Hard Refresh
    1. max-age=0 vs no-cache
  5. Revalidation
  6. When to Use a Cache-Control Request Header
    1. Realtime Data
    2. Offline Applications
  7. Final Takeaways

I’ve written and spoken many, many times about the Cache-Control response header and its many directives, but one thing I haven’t covered before—and something I don’t think many developers are even aware of—is the Cache-Control request header. Unless you know your caching well, those two links in the first sentence will make this article a lot easier to understand. Maybe pop them open in another tab as a reference.

Let’s go!

Cache-Control Recap

As developers, we’re most used to Cache-Control as the preferred way of instructing caches (usually browsers) on how they should store responses (if at all), and what to do once their cache lifetime is up. Maybe something like this:

Cache-Control: max-age=2147483648, immutable

There’s a lot more to it than that, which you can read about in my 2019 piece, Cache-Control for Civilians

One thing we’re probably less used to is Cache-Control’s employment as a request header.

In a nutshell, the Cache-Control request header determines whether the browser retrieves content from the cache or forces a network request. It’s also used by intermediaries such as CDNs to work out whether they should serve a response themselves, or keep passing the request back upstream to origin.

It’s a way for the client to force freshness.

This means that the most common way you’re ever likely to see a Cache-Control request header is when you refresh or hard refresh a page. Honestly, that’s mostly it.

All browsers behave a little differently between refreshes, hard refreshes, and run of the mill revalidation.

Refresh

In Chrome, even if the page is still fresh, refreshing it will dispatch a request to the network with the following request headers:

Cache-Control: max-age=0
[If-Modified-Since|If-None-Match]
  • Cache-Control: max-age=0 just means after zero seconds, revalidate this resource. This isn’t incredibly strictly enforced so it’s technically a weak instruction to revalidate. More on that later.
  • The If-Modified-Since or If-None-Match headers are revalidation request headers that are used to compare the current version of the response with the target version on the network.

All other subresources on the page are fetched as per their caching headers, so there is no different or specific behaviour here.

In Firefox the behaviour is a little different. Refreshing a still-fresh page results in the following request headers:

[If-Modified-Since|If-None-Match]

No Cache-Control request header at all, just the relevant revalidation headers.

Again, all of the page’s subresources are treated as normal.

Safari is different still, and generally seems much more aggressive with its cache busting. Refreshing the same still-fresh page in Safari gives the following request headers:

Cache-Control: no-cache
Pragma: no-cache

We have a Cache-Control request header, this time with a no-cache directive. While this is functionally equivalent to max-age=0, the spec speaks much more clearly that no-cache means that a cache MUST NOT use the response to satisfy a subsequent request without successful revalidation with the origin server. We also have the first appearance of Pragma, also carrying no-cache. Pragma is an incredibly outdated header that serves as a backward compatibility measure for HTTP/1.0 caches. Safari including this here is a very defensive measure!

Again, all other subresources are treated as they would be normally.

All browsers exhibit some similarities and some differences.

  • Even if the main document was still fresh in HTTP cache, a refresh will always put a request out onto the network in all browsers.
  • Chrome and Firefox emit revalidation headers which mean that, even though we’ve refreshed the page, we might still get served our locally cached version (304) if it’s still valid.
  • Firefox doesn’t emit a Cache-Control header, making it the least aggressive of the three.
  • Safari is by far the most aggressive, emitting both Cache-Control and Pragma headers, and no revalidation headers for potential reuse. Safari will always return a 200 response to a refresh.

Hard Refresh

Things are a little different when it comes to a hard refresh. Hard refreshes are usually a sign of user frustration and that something is badly broken or outdated. To this end, browsers begin upping the ante here.

In all browsers, a hard refresh causes both the main document and all of its subresources to be requested with the following:

Cache-Control: no-cache
Pragma: no-cache

Key things to note:

  • No browser emits a revalidation header, meaning a 304 is not possible. We’re always guaranteed a fresh response.
  • Chrome switched from max-age=0 to no-cache. This is a clear signal of intent that a hard refresh is more aggressive than a regular one.
  • Safari’s behaviour remains unchanged, which means that as far as the main document is concerned, a refresh and a hard refresh are equivalent.

Note that this all applies to the main document and all of its subresources—even immutable assets—so everything on the page is now guaranteed fresh. 304 responses are not possible.

max-age=0 vs no-cache

Both of these directives behave incredibly similarly: max-age=0 means the response is considered stale after zero seconds and therefore should be revalidated, and no-cache means don’t fetch this response from cache without revalidating it first.

Where they differ is that max-age=0 permits caches to reuse a response if revalidation isn’t possible (e.g. no network access); no-cache is much stricter—it means the cache must always revalidate before releasing a response, or return an error if revalidation fails.

Revalidation

In the case that a user hasn’t refreshed the page, but instead they have a file in their cache that is now considered stale, the browser needs to check with the server whether or not it needs a new copy, or if it can reuse and renew the previously cached version. This is called revalidation and is when the If-Modified-Since or If-None-Match headers come into play.

  • If-Modified-Since is used to check a file against its Last-Modified response header.
  • If-None-Match is used to check a file against its Etag response header.

When a file needs revalidating, all browsers behave the same:

[If-Modified-Since|If-None-Match]

They attach the relevant revalidation header, which will result in either a 200 or 304 response in most cases. This is unremarkable other than the fact that no browser attaches a Cache-Control request header at this point.

Each of these use cases was browser-defined, very much out of our hands as web developers, but there are scenarios when we might want to (and can!) add our own Cache-Control request headers. Think of these scenarios as incredibly aggressive, incredibly defensive bidirectional caching rules to absolutely guarantee that no caches anywhere along with request–response lifecycle will retain a copy of a response. By setting Cache-Control at both ends, we have a double-pronged approach to our strategy. A very cautious approach.

Realtime Data

Imagine you’re building a sports betting site or stock trading app: realtime price updates are incredibly important, and all data must be up to date, always. You’d serve your responses with something like:

…and make your requests with something like:

fetch("https://api.website.com/data", {
  method: "GET",
  headers: {
    "Cache-Control": "no-store",
  }
})

This is the bare minimum for modern and compliant caches, but the hyper-defensive version would be more like:

Cache-Control: no-store, no-cache, max-age=0, must-revalidate
Pragma: no-cache

…on your responses, and this in your requests:

fetch("https://api.website.com/data", {
  method: "GET",
  headers: {
    "Cache-Control": "no-store, no-cache, max-age=0",
    "Pragma": "no-cache"
  }
})

The latter two examples are overkill and do contain a lot of redundancy, but they also won’t do any harm.

Note that if the data is also potentially sensitive and contains user-specific data, you’d want to add private to your Cache-Control response headers.

Offline Applications

If you have an offline application, you can use only-if-cached to only serve a response if it’s in cache, otherwise returning a 504.

fetch("https://api.website.com/offline-data", {
  method: "GET",
  headers: {
    "Cache-Control": "only-if-cached"
  }
})

Adding this request header ensures that the request would never hit the network.

While only-if-cached might not be useful for most web pages, it can be handy for offline-first applications, such as PWAs or news readers that prefer using stored content rather than attempting a network request that might fail.

Final Takeaways

  • Browsers automatically send a mix Cache-Control, Pragma, or revalidation headers in refresh and hard refresh scenarios.
  • max-age=0 and no-cache both trigger revalidation, but no-cache is much stricter and requires a fresh response.
  • You can manually use Cache-Control in requests when you need realtime data or offline-first apps.
  • To force freshness, use:
    Cache-Control: no-store, no-cache, max-age=0
    
  • To build offline-first apps, consider:
    Cache-Control: only-if-cached
    

Need a helping hand with your caching strategy? Schedule a performance audit.


Frequently Asked Questions

When should I add Cache-Control in HTTP requests?

Use it for real-time or offline-first data where freshness is critical; e.g. `no-store` or `only-if-cached`.

Is `max-age=0` the same as `no-cache`?

Both force revalidation, but `no-cache` is stricter and must revalidate before reuse.