HTTP Caching 101

godinhojoao

João Godinho

Posted on June 19, 2024

HTTP Caching 101

Table of Contents

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:
    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
Enter fullscreen mode Exit fullscreen mode
  • 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: 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: 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.
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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
    • Cache Poisoning: Inserting malicious content into a cache to affect multiple users.
    • 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: "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:

Thanks for Reading!

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

Contacts

💖 💪 🙅 🚩
godinhojoao
João Godinho

Posted on June 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

HTTP Caching 101
webdev HTTP Caching 101

June 19, 2024