HTTP Caching 101
Tags: webdev, http, cache, basic
Published: 2024-06-19 22:50:30

## Table of Contents

- [First of all: What is Caching?](#first-of-all-what-is-caching)
- [What is HTTP Caching? And when should we use it?](#what-is-http-caching-and-when-should-we-use-it)
- [Heuristic caching](#heuristic-caching)
- [Cache-Control HTTP header: Used to define caching policy on HTTP services.](#cache-control-http-header-used-to-define-caching-policy-on-http-services)
  - [public, private, no-cache, and no-store directives](#public-private-no-cache-and-no-store-directives)
  - [max-age and immutable directive](#max-age-and-immutable-directive)
- [Validation](#validation)
  - [Last-Modified and If-Modified-Since headers](#last-modified-and-if-modified-since-headers)
  - [ETag header](#etag-header)
  - [Which one should I use: ETag or Last-Modified?](#which-one-should-i-use-etag-or-last-modified)
- [Cache busting](#cache-busting)
- [Request collapsing](#request-collapsing)
- [Security](#security)

## First of all: What is Caching?

- The process of storing copies of files in a temporary high-speed storage, known as cache. It allows you to efficiently reuse previously retrieved or computed data.
- Cache is generally stored in fast-access hardware such as RAM. Its primary purpose is to increase data retrieval performance by reducing the need to access the underlying slower storage layer such as hard disk drives (HDDs) or solid-state drives (SSDs).
- The trade-off when using a cache is sacrificing capacity and durability for increased speed and efficiency.
- Cache can be applied in many layers, for example: operating systems, networking (CDNs and DNS), web applications, and databases, significantly reducing latency and improving performance for read-heavy workloads and compute-intensive tasks.
- [AWS Cache for each layer](https://aws.amazon.com/caching/):
  <img src="https://i.ibb.co/5121NnM/aws-cache-by-layer.png" alt="AWS Cache for each layer image"/>

- There are many **benefits of caching**, such as: Improving Application Performance, Eliminate Database Hotspots, Increase Read Throughput IOPS (Input/Output operations per second), and more.

## What is HTTP Caching? And when should we use it?

- The HTTP cache stores a response associated with a request and **reuses the stored response for subsequent requests**.
- HTTP cache can be handled by the **user's browser (private cache)** or also by **Content Delivery Networks (CDNs) that act as (intermediary caches)** before accessing our Origin server. Improving performance, and reducing costs.
  - An **intermediary cache** means that **it's between the client and the origin server**. This includes CDNs, proxy servers, and even some load balancers that might have caching capabilities.
- **When should we use HTTP Caching?**
  - To cache static assets such as images, CSS, and other resources that rarely change.
  - To cache responses that don't change often to reduce server load. Reducing the number of requests to the origin server, improving scalability, reducing costs, and improving user experience because of the performance.

## Heuristic caching

- By default, HTTP caches responses even without explicit Cache-Control headers. This is called heuristic caching, a legacy approach. Heuristic caching is still functional in most modern browsers, but it's generally considered a less reliable and outdated approach.
  - It's strongly recommended to use Cache-Control headers for better control and consistency
- In summary, it works using other headers like `Last-Modified` to guess how long to cache the response. It's made by the web browser and can also not work for some browsers.

## Cache-Control HTTP header: Used to define caching policy on HTTP services.

- `Cache-Control Directives` control who caches the response, under what conditions, and for how long. There are many directives that we can use to specify the "cache configuration" of a specific resource on our service.

- ## public, private, no-cache, and no-store directives:

  - `public`: **any cache may store the response** (all users).
  - `private`: cache is for a single user and **must not be stored by a shared cache**.
    - **Private responses can’t be stored by CDNs** like AWS CloudFront but **can be stored by the browser** which is considered a private cache.
    - This is useful for login methods and endpoints in which the content is personalized for each user, to prevent sharing data with other users.
   - `no-cache`: prevents the reuse of responses without revalidation, it means "revalidate the cache before caching it again for future requests!". _So, how to **literally** avoid cache?_ We will see it later.
  - `no-store`: avoids using a response already stored in any cache.

- ## max-age and immutable directive

  - `max-age`: Time in seconds that you will cache some API response, for example, max-age=30, means that after 30 seconds it will consider the cached data stale and it will request the origin server to fetch this data again. Use max-age=0 to invalidate the cache and get data from the origin server.
    - `s-maxage`: The "s-" means "shared cache". This directive is explicitly for CDNs. It overrides the max-age directive and expires the header field when present.
    - Two states of HTTP responses: `fresh` cache is valid and `stale` cache is invalid. This state is changed using the `max-age` directive.
    - When caching content that the user can access just after authentication, be sure to use `max-age <= auth expiration time`.
  - `immutable`: Even using a big `max-age`, when the user reloads some browsers will revalidate the cache, and to solve this you can say to your browser that this content is `immutable`.

```
Cache-Control: public, max-age=31536000, immutable
```

- ## How to don't cache?

 - It's not recommended to use no-store too broadly. Because you lose many advantages that HTTP and browsers have, including the **browser's back/forward cache**.
     - [Bfcache](https://nitropack.io/blog/post/back-forward-cache): is a feature that allows browsers to create and store a snapshot of an already visited web page in their in-memory
 - To avoid using recently saved cache without validation you can use `no-cache` in combination with `private`.
     - In old services using versions before HTTP/1.1 that don't understand `no-cache` you can use `max-age=0`.

## Validation

- Stale responses are not immediately discarded.
- HTTP can **transform a stale response into a fresh one by asking the origin server**. This is called validation, or sometimes, revalidation.
- Validation is done by using a **conditional request** that includes an `If-Modified-Since` or `If-None-Match` request header.

- ## Last-Modified and If-Modified-Since headers:

  - `Last-Modified`: Date and time the origin server believes the data was last modified, it's set by the origin server, not the clients. It's used together with **If-Modified-Since** header.
  - `If-Modified-Since`: If the requested data has not been modified since the specified time, the server will return HTTP status code 304 (not modified). In this case, the data can be used from cache (CDN or Browser).

    - **Observations**: Last-Modified is returned by the Origin server and If-Modified-Since time is interpreted by the server, whose clock might not be synchronized with the client. So the best strategy for a client is to reuse the exact value of `Last-Modified` as your `If-Modified-Since`.

  - Example summarized from MDN Web Docs:
    <img src="https://i.ibb.co/rQ92Xzv/http-last-modified-and-if-modified-since-headers.png" alt="HTTP Last-Modified and If-Modified-Since headers flowchart"/>

    - The server can manage the last modified time, but there are some issues with this approach. Parsing the time format can cause issues, and distributed servers often struggle to keep file update times synchronized.
    - To solve such problems, the **ETag** response header was standardized as an alternative. Together with the **If-None-Match**.

- ## ETag with If-None-Match header:

  - `ETag`: Works like a cache identifier, can be a number, or any string and hash. Means the version of a resource.
  - `If-None-Match`: "Hey server, if no cache identifier matches with this that I'm sending, please give me a new response."

  ```
  # response was sent with this ETag and it was cached for max-age=3600 (3600secs = 60min)
  HTTP/1.1 200 OK
  Content-Type: text/html
  Content-Length: 1024
  Date: Tue, 22 Feb 2022 22:22:22 GMT
  ETag: "33a64df5"
  Cache-Control: max-age=3600

  # request gets stale after 60min and sends a request with this If-None-Match
  GET /index.html HTTP/1.1
  Host: example.com
  Accept: text/html
  If-None-Match: "33a64df5"

  # If this ETag or "cache identifier" is still valid: Response is returned with 304 not modified and the cache is valid for more 3600secs.
  # If not, the new response is returned with 200 success, and a new ETag is returned to be cached.
  ```

- ## Which one should I use: `ETag` or `Last-Modified`?

  - Summarizing: You should use both, but the `ETag` and the `If-None-Match` takes precedence over `Last-Modified` and `If-Modified-Since`.
  - **MDN Web docs note**: RFC9110 prefers that servers send both ETag and Last-Modified for a 200 response if possible. During cache revalidation, if both If-Modified-Since and If-None-Match are present, then If-None-Match takes precedence for the validator. If you are only considering caching, you may think that Last-Modified is unnecessary. However, Last-Modified is not just useful for caching; it is a standard HTTP header that is also used by content-management (CMS) systems to display the last-modified time, by crawlers to adjust crawl frequency, and for other various purposes. So considering the overall HTTP ecosystem, it is better to provide both ETag and Last-Modified.

## Cache busting

- Is a technique to cache static files for a long time by attaching a version to the URL. It is good for caching for **long periods**.
- Web browsers cache locally static files like JS, CSS, etc... The problem is that these files change frequently during the development. So, if you use only `max-age` to cache these files the users will not have the most consistent version of it.
- `Cache busting` solves this by using different URLs for each version, ensuring updated content when deploying a new version.

```
# version in filename
bundle.v123.js

# version in query
bundle.js?v=123

# You can also use a hash or anything as your version identifier.
# Note that you can use a long max-age and immutable combined with URL versions
```

- **When not use cache busting?**
  - For main resources that changes frequently because of server-side logic or user interaction like `index.html`.
  - If your website is static, you can also use cache busting on `index.html`.

## Request collapsing

- When multiple identical requests, using the same `cache key` (an identifier for your current cache), and no valid cache is stored.
- The first request receives a response with `cache miss` and the others will use the same response content but with `cache hit`.
- Reducing the load of your origin server, since only one request reaches it.
- In **some resources** (e.g. auth) for **security reasons**, you may need to **avoid request collapsing**. You can achieve this by using specific headers, such as:
  - `Cache-Control: private`, `Cache-Control: no-store`, `Cache-Control: no-cache`, `Cache-Control: max-age=0`, or `Cache-Control: s-maxage=0`.

## Security

- I recommend you to read some contents at [RFC 9111 HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111)
  - [Cache Poisoning](https://www.rfc-editor.org/rfc/rfc9111#name-cache-poisoning): Inserting malicious content into a cache to affect multiple users.
  - [Timing attacks](https://www.rfc-editor.org/rfc/rfc9111#name-timing-attacks): Exploiting cache behavior to infer user actions based on resource loading times.
- "Ops, I've saved something that I shouldn't, using a long `max-age`"
  - [Deleting stored responses](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#deleting_stored_responses): "There is basically no way to delete responses that have already been stored with a long max-age.".
  - So be sure before setting your headers and working with the http cache.

## References:

- https://aws.amazon.com/caching/
- https://www.cloudflare.com/learning/cdn/what-is-caching/
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
- https://software-factotum.medium.com/effective-http-caching-part-i-62be61280c8c
- https://software-factotum.medium.com/effective-http-caching-part-ii-e1172093d041
- https://software-factotum.medium.com/effective-http-caching-part-iii-public-private-and-no-store-b64f0452325
- https://bunny.net/academy/http/what-is-caching-and-how-does-cache-work/
- https://nitropack.io/blog/post/back-forward-cache
- https://www.rfc-editor.org/rfc/rfc9111
- https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html#request-custom-traffic-spikes
- https://odino.org/http-cache-101-scaling-the-web/
- https://odino.org/rest-better-http-cache/

## Thanks for Reading!

- Feel free to reach out if you have any questions, feedback, or suggestions. Your engagement is appreciated!

## Contacts

- You can find this and more content on:
  - [My website](https://godinhojoao.com/)
  - [GitHub](https://github.com/godinhojoao)
  - [LinkedIn](https://www.linkedin.com/in/joaogodinhoo/)
  - [Dev Community](https://dev.to/godinhojoao)