October 20, 2022

HTTP Cache Headers

Best Practices for managing the static resource cache and using the correct cache headers as Cache-Control.

There are many different HTTP headers that can be used by your website. Right now, we'll focus on a specific subset of these: cache headers. We'll show you the details when it comes to cache headers, so you'll know how to use them in the future!

In the context of websites and apps, caching is defined as storing content in temporary memory, such as that on the user's browser or device or on an intermediate server, to reduce the time it takes to access that file .

According to HTTP Archive, among the top 300.000 sites, the user's browser can cache nearly half of all downloaded content.

Reduces the time it takes for the user to view images or Javascript or CSS files. This is because the user is now accessing the file from his system instead of being downloaded from the network. At the same time, caching also reduces the number of requests and data transfer from your servers. Undoubtedly this is a huge savings for views and repeat page visits.

How does caching work?

Suppose we open a web page https://www.example.com and the server returns the following HTML code:

<link type="text/css" href="https://www.example.com/app.css" rel="stylesheet">
<!-- Rest of the HTML -->

When the browser parses this HTML, it identifies that a CSS resource needs to be loaded from https://www.example.com/app.css .

The browser sends a request to the server for this file, the server returns the file and also tells the browser to cache it for 30 days. Later in this guide, we'll cover how the server sends these instructions to the browser.

GET app.css, no objects found in local cache

Now, let's say you open another page on the same website after a few hours. The browser parses the HTML again and finds the same CSS file on this page as well: https://www.example.com/app.css . Since the browser has this particular resource available in its local cache, it won't even go to the server. The network request is never made, the file is accessed from the local cache and the styles are applied very quickly.

How is it done?

There is a range of cache headers and related directives that provide explicit instructions to any end-user server, CDN, and browser on how to handle cached content. Combining multiple features can give you the desired results or they may cause the cache to not work because they are being used incorrectly. We'll give you a quick overview of the most common HTTP cache headers, their directives, and how to use them.

Cache-Control: directives

There are multiple directives that can be used to control the functionality of the cache headers. You don't have to use them all, so pick the ones you need and leave the others out. These are the most important directives:


In most uses of the cache control headers you will see this directive in use. Indicates the maximum amount of time in seconds that retrieved responses can be reused from the time a request is made. For example: max-age=300indicates that a resource can be reused for the next 300 seconds. This resource can be cached by the browser or any cache downstream of the server for this period of time.


Il s-prefix “” stands for shared as in shared cache. This directive is explicitly for CDN among other intermediate caches. This directive overrides the max-agedirective and expires the header field when present. It is important to remember that this directive will not affect visitors' browsers. So you can use different values ​​for mag-ages-maxagespecify different cache times for both visitors and CDNs.

It can be useful, for example if you use caching systems such as Varnish and want to instruct the cache server to set a certain cache time for some pages.


La no-cachedirective shows that the returned responses cannot be used for subsequent requests to the same URL before checking if the server responses have been modified. With an appropriate ETag (also known as a validation token) present, consequently, it occurs no-cachea roundtrip in an attempt to validate cached responses. However, caches can suppress downloads if resources have not changed. This means that web browsers may cache assets but must check every request if the assets have changed (the server will return an HTTP 304 response if nothing has changed).

no store

Unlike no-cacheno-storedirective is simpler. This is because it prevents browsers and all intermediate caches from storing any version of the replies returned, such as replies containing private / personal information or banking information. Whenever users request this asset, requests are sent to the server. Resources are downloaded every time.

public insurance

If a response is marked public, it can be cached even in cases where it is associated with HTTP authentication or the HTTP response status code is not normally cacheable. Because explicit caching information, such as via the max-agedirective, show that a response is still cacheable, using this directive is usually not necessary.

In most cases, a response marked as public is unnecessary, since explicit caching information (eg. max-age) show that a response is still cacheable.


A response marked as private can be cached by the browser. However, these responses are generally intended for single users, so they are not cacheable from intermediate caches (e.g. HTML pages with private user information may be cached by a user's browser but not from a CDN).


Long ago this header was used to exploit caching mechanisms. This header simply contains a date and time stamp. It is still useful for older user agents, but it is important to note that the Cache-Control max-agehave s-maxagestill precedence on most modern systems.


This header is one of the most common validators for the cache. Indicates when a requested resource was last modified. While it is one of the most common validators, its origins date back to the HTTP / 1.0 era, making it seen by some as a legacy validator.


A newer validation method is the use of ETag. This has become the standard since HTTP / 1.1. This is validated via an ETag header field. It is usually based on a hash of the requested content, but it doesn't have to be. However, the client requesting it must have no knowledge of how it is generated. If a client has an object in its cache that has expired, it can use the ETag to send an HTTP request to the server. The server will then check this token against cached resources. If the asset has not been modified, the server can return an unchanged 304 response to the client. This will regenerate the lifetime of the cached resource, instead of re-loading the resource.

Etag can be a replacement or complementary validation header to last-modified. It can be particularly useful to implement this when dealing with incorrect last-modified headers or need to disable them by removing the header.

Let's see some practical cases

Any type of caching would normally work through updating and validation. Let's explain what it means. New requests will typically give you a fresh copy of the content served instantly from the cache. However, a validated representation will rarely resend the entire copy if it hasn't changed since you last requested it. In cases where there is no validator present (e.g. an ETag or Last-Modified header) combined with a lack of freshness information, it will usually be considered non-cacheable.

Achieving proper caching offers huge performance benefits, saves bandwidth, and lowers server costs, but many sites use half of the caching, creating competition conditions resulting in a loss of synchronization of interdependent resources.

The vast majority of best practice caching falls into one of two models:

Scheme 1: immutable content + max-age

Cache-Control: max-age = 31536000
  • The content of this URL never changes, so ...
  • The browser / CDN can cache this resource for one year without problems
  • The cached content of less than max-ageseconds can be used without consulting the server

In this template, you never change the content in a certain URL, you change the URL:

<script src="/script-f93bca2c.js"></script>
<link rel="stylesheet" href="/styles-a837cb1e.css" />
<img src="/cats-0e9a2ef4.jpg" alt="" />

Each URL contains something that changes along with its content. It could be a version number, modification date or a hash of the content, which is what I do on this blog.

However, this model doesn't work for things like articles and blog posts. Their URLs cannot be versioned and their content must be able to change. Seriously, given the basic grammar and spelling mistakes I make, I need to be able to update content quickly and frequently.

Scheme 2: editable content, always revalidated by the server

Cache-Control: no-cache
  • The content of this URL may change, so ...
  • Any locally cached version is not trusted without server permission

Note: no-cache it does not mean "do not cache", it means that it must check (or "revalidate" as it calls it) with the server before using the cached resource. no-storetells the browser not to cache it at all. Furthermore must-revalidateit does not mean “must revalidate”, it means that the local resource can be used if it is younger than the one provided max-age, otherwise it must be revalidated. Yes I know.

In this template you can add a header ETag(a version ID of your choice) or one Last-Modifiedgiven on reply. The next time the client fetches the resource, it echoes the value for the content it already has through If-None-MatchIf-Modified-Sincerespectively, allowing the server to say "Use only what you already have, it is up to date" or as you spell "HTTP 304".

If sending ETagLast-Modifiedthis is not possible, the server always sends the entire content.

This model always involves a network recovery, so it is not as good as model 1 which can bypass the network completely.

It is not uncommon to be put off by the infrastructure needed for Model 1, but likewise to be put off by the network demand required by Model 2 and instead choose something in between: a max-agesmall and changeable content. This is a nice compromise.

max-age on changing content is often the wrong choice

… And unfortunately it is not uncommon, for example it happens on the pages of Github.

To imagine:

  • /article/
  • /styles.css
  • /script.js

... all served with:

Cache-Control: must be revalidated, max-age = 600
  • The content of the URLs changes
  • If your browser has a cached version of less than 10 minutes, use it without consulting the server
  • If not, perform a network recovery, using If-Modified-SinceIf-None-Matchif available

This model may seem to work during testing, but it breaks things down in the real world and is really hard to track down. In the example above, the server had actually updated the HTML, CSS, and JS, but the page ended up with the old HTML and JS from the cache and the updated CSS from the server. Version mismatch broke things.

Often, when we make significant changes to the HTML, we are likely to also modify the CSS to reflect the new structure and update the JS to accommodate the style and content changes. These resources are interdependent, but the caching headers cannot express it. Users may end up with the new version of one / two of the resources, but the old version of the other (s).

max-ageit is relative to response time, so if all the above resources are required as part of the same navigation they will be set to expire around the same time, but there is still a small chance of a race there. If you have some pages that don't include JS or include different CSS, the expiration dates may not be in sync. And worse, the browser continually deletes things from the cache and doesn't know that HTML, CSS, and JS are interdependent, so it will happily release one but not the other. Multiply all of this together and it is not unlikely that you will end up with mismatched versions of these resources.

For the user, this can cause broken layout and / or functionality. From small defects to completely unusable content.

Do you have doubts? Don't know where to start? Contact us!

We have all the answers to your questions to help you make the right choice.

Chat with us

Chat directly with our presales support.


Contact us by phone during office hours 9:30 - 19:30

Contact us online

Open a request directly in the contact area.


Managed Server Srl is a leading Italian player in providing advanced GNU/Linux system solutions oriented towards high performance. With a low-cost and predictable subscription model, we ensure that our customers have access to advanced technologies in hosting, dedicated servers and cloud services. In addition to this, we offer systems consultancy on Linux systems and specialized maintenance in DBMS, IT Security, Cloud and much more. We stand out for our expertise in hosting leading Open Source CMS such as WordPress, WooCommerce, Drupal, Prestashop, Joomla, OpenCart and Magento, supported by a high-level support and consultancy service suitable for Public Administration, SMEs and any size.

Red Hat, Inc. owns the rights to Red Hat®, RHEL®, RedHat Linux®, and CentOS®; AlmaLinux™ is a trademark of AlmaLinux OS Foundation; Rocky Linux® is a registered trademark of the Rocky Linux Foundation; SUSE® is a registered trademark of SUSE LLC; Canonical Ltd. owns the rights to Ubuntu®; Software in the Public Interest, Inc. holds the rights to Debian®; Linus Torvalds holds the rights to Linux®; FreeBSD® is a registered trademark of The FreeBSD Foundation; NetBSD® is a registered trademark of The NetBSD Foundation; OpenBSD® is a registered trademark of Theo de Raadt. Oracle Corporation owns the rights to Oracle®, MySQL®, and MyRocks®; Percona® is a registered trademark of Percona LLC; MariaDB® is a registered trademark of MariaDB Corporation Ab; REDIS® is a registered trademark of Redis Labs Ltd. F5 Networks, Inc. owns the rights to NGINX® and NGINX Plus®; Varnish® is a registered trademark of Varnish Software AB. Adobe Inc. holds the rights to Magento®; PrestaShop® is a registered trademark of PrestaShop SA; OpenCart® is a registered trademark of OpenCart Limited. Automattic Inc. owns the rights to WordPress®, WooCommerce®, and JetPack®; Open Source Matters, Inc. owns the rights to Joomla®; Dries Buytaert holds the rights to Drupal®. Amazon Web Services, Inc. holds the rights to AWS®; Google LLC holds the rights to Google Cloud™ and Chrome™; Microsoft Corporation holds the rights to Microsoft®, Azure®, and Internet Explorer®; Mozilla Foundation owns the rights to Firefox®. Apache® is a registered trademark of The Apache Software Foundation; PHP® is a registered trademark of the PHP Group. CloudFlare® is a registered trademark of Cloudflare, Inc.; NETSCOUT® is a registered trademark of NETSCOUT Systems Inc.; ElasticSearch®, LogStash®, and Kibana® are registered trademarks of Elastic NV Hetzner Online GmbH owns the rights to Hetzner®; OVHcloud is a registered trademark of OVH Groupe SAS; cPanel®, LLC owns the rights to cPanel®; Plesk® is a registered trademark of Plesk International GmbH; Facebook, Inc. owns the rights to Facebook®. This site is not affiliated, sponsored or otherwise associated with any of the entities mentioned above and does not represent any of these entities in any way. All rights to the brands and product names mentioned are the property of their respective copyright holders. Any other trademarks mentioned belong to their registrants. MANAGED SERVER® is a trademark registered at European level by MANAGED SERVER SRL, Via Enzo Ferrari, 9, 62012 Civitanova Marche (MC), Italy.


Would you like to see how your WooCommerce runs on our systems without having to migrate anything? 

Enter the address of your WooCommerce site and you will get a navigable demonstration, without having to do absolutely anything and completely free.

No thanks, my customers prefer the slow site.
Back to top