Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 29

Server-Side HTTP Caching

Recommended resources

Hello! Welcome to the sixth lesson in our module on Web Servers and Application Runtimes.

In our last lesson, we demystified the "glue" that connects a web server like Gunicorn to a Python application like Django: the WSGI and ASGI specifications. We now have a complete picture of the request lifecycle: a client request hits Nginx, which acts as a reverse proxy and forwards it to Gunicorn. Gunicorn then uses the WSGI/ASGI interface to invoke our Django application, which processes the request and returns a response.

Executing this entire chain for every single request can be resource-intensive, especially when the application needs to perform database queries or complex computations. What if the content of a response doesn't change frequently? It would be far more efficient to avoid hitting the application server at all.

This brings us to today's topic. Our learning outcome is to describe HTTP caching mechanisms controlled by the server, including Cache-Control headers and ETags. We will explore how your backend server can instruct browsers and other intermediaries (like CDNs or proxies) to store and reuse responses, dramatically improving performance and reducing server load.

Why Cache? The Fundamentals

At its core, web caching is about storing a copy of a resource (like an HTML page, a JSON response, or an image) and serving that copy for subsequent requests instead of fetching it from the source, or "origin server," every time.

The benefits are significant:

  • Faster Response Times: Serving content from a local browser cache or a nearby proxy is much faster than going all the way to your server.
  • Reduced Server Load: By intercepting requests before they reach your application, caching reduces the CPU and memory usage of your Gunicorn workers and the load on your database.
  • Decreased Network Bandwidth: Sending fewer responses means less data travels over the network, which can lower costs.

Caches can exist in several places, but for our purposes, the main two are:

  1. Private Caches: These are dedicated to a single user. The most common example is your browser's cache.
  2. Shared Caches: These store responses that can be reused by multiple users. Examples include proxies within a company or a Content Delivery Network (CDN).

To get a solid grasp of these core concepts and the terminology we'll be using, let's start with a short video.

Everything you need to know about HTTP Caching

The video 'Everything you need to know about HTTP Caching' from the roadmap channel provides an excellent introduction to why we cache and the basic terminology.

Please watch from the beginning until 00:03:09. Focus on understanding the benefits of caching and the definitions of key terms like 'stale content', 'fresh content', and 'content validation'.


The Expiration Model: Cache-Control

The primary way a server controls caching is through the expiration model. The server attaches an HTTP header to the response that tells caches how long they can consider the response "fresh." During this freshness lifetime, the cache can serve the stored response directly without contacting the origin server.

The modern and most powerful header for this is Cache-Control. It's a multi-value header that accepts several directives to fine-tune caching behavior.

Here are the most critical directives you'll use:

  • max-age=<seconds>: Specifies the maximum amount of time, in seconds, that a response is considered fresh from the time it was generated. This is the cornerstone of expiration-based caching.
    • Example: Cache-Control: max-age=3600 means the response is fresh for one hour.
  • public vs. private:
    • public: Indicates that the response can be stored by any cache, including shared caches (like a CDN).
    • private: Indicates the response is for a single user and should only be stored by private caches (like a browser). This is essential for user-specific content.
  • no-store vs. no-cache: This is a frequent point of confusion.
    • no-store: The "nuclear option." It instructs all caches not to store the response at all. Every request for this resource must go to the origin server. Use this for highly sensitive data.
    • no-cache: This is misnamed. It does not mean "don't cache." It means the response can be stored, but it must be re-validated with the origin server before each use. We'll see how this validation works in the next section.
  • s-maxage=<seconds>: Similar to max-age, but it applies only to shared caches. It overrides max-age for those caches, allowing you to have different caching rules for CDNs and browsers.
  • must-revalidate: This directive tightens the rules. It tells caches that once a resource becomes stale, they must successfully revalidate it with the origin server. They are not allowed to serve the stale content under any circumstances (e.g., during a network outage).

The older Expires header also sets a freshness lifetime, but it uses an absolute date and time. Cache-Control: max-age is more flexible and is given precedence by modern caches.

Let's continue with the video to see these directives in action.

Everything you need to know about HTTP Caching

This next segment of the video explains the most important Cache-Control directives in detail.

Watch from 00:03:09 to 00:08:48. Pay close attention to the difference between public and private, and especially no-store versus no-cache.

For a more comprehensive and technical breakdown, the following article is an excellent reference. It clarifies many common misconceptions.

A complete guide to HTTP caching

The article 'A complete guide to HTTP caching' by Jono Alderson is a superb deep-dive. We'll focus on the sections that explain the core headers and the common gotchas.

Please read two parts of this article: The subsection titled 'The Cache-Control (response) header' under 'Core HTTP caching headers'. The section titled 'Common misconceptions & gotchas', paying special attention to the explanations for 'no-cache ≠ “don’t cache”' and 'max-age=0 vs must-revalidate'. This will solidify your understanding of these subtle but critical directives.


The Validation Model: ETag

The expiration model is great, but what happens when a cached resource becomes stale? The client could simply request the full resource again, but what if the resource hasn't actually changed? That would be a waste of bandwidth.

This is where the validation model comes in. It allows a cache to ask the origin server, "Is the version I have still good?" without re-downloading the entire resource.

The most effective mechanism for this is the ETag (Entity Tag) header. Here's the flow:

  1. When the server sends a response, it includes an ETag header with a unique identifier for that specific version of the resource. This identifier is typically a hash of the content.
    HTTP/1.1 200 OK
    Content-Type: application/json
    Cache-Control: max-age=60
    ETag: "abc-123-xyz-789"

  2. The cache stores the response along with its ETag. After 60 seconds, the response is stale.

  3. The next time the client needs the resource, it makes a conditional request to the server, using the If-None-Match request header with the ETag it has stored.
    GET /api/products
    If-None-Match: "abc-123-xyz-789"

  4. The server receives this request, regenerates the ETag for the current version of the resource, and compares it to the one sent by the client.

    • If the ETags match: The resource hasn't changed. The server responds with a 304 Not Modified status code and an empty body. This is extremely efficient, as only headers are transmitted. The cache can then mark its stored version as fresh again and serve it.
    • If the ETags do not match: The resource has changed. The server responds with a 200 OK, the new resource content, and a new ETag header.

This combination of Cache-Control (for freshness) and ETag (for validation) is the foundation of modern, efficient HTTP caching.

HTTP Caching Decision Flowchart
This flowchart shows the decision process for caching. The server first decides if a resource is reusable. If so, it decides if revalidation is needed (`no-cache`) or if it can be considered fresh for a time (`max-age`). Finally, an `ETag` is added to support efficient revalidation once the resource becomes stale.

For a deep dive into ETags, including a very practical problem you might encounter in a production environment, the following video is excellent.

HTTP Caching with E-Tags - (Explained by Example)

Hussein Nasser's channel provides deep technical explanations. This video focuses entirely on ETags, their benefits, and a common pitfall when using them with load balancers—a perfect example of the 'root level' details you're looking for.

Please watch two segments: 00:00:43 to 00:04:24: This explains the fundamental client-server interaction using ETag and If-None-Match. 00:07:10 to 00:10:08: This covers a crucial real-world problem where load balancers can cause ETag validation to fail, and discusses the solution. This will give you a robust understanding of how ETag works and its operational considerations.


Caching Strategies in Practice

How you configure these headers depends entirely on the nature of the resource. There is no one-size-fits-all solution.

  • Versioned Static Assets (e.g., main.a4b2c1.css, logo-v3.png):

    • The filename itself contains a version hash. When the file changes, the URL changes.
    • Strategy: Cache aggressively and immutably.
    • Cache-Control: public, max-age=31536000, immutable
    • This tells browsers to cache the file for one year (31536000 seconds) and, because it's immutable, to never even bother revalidating it.
  • Unversioned Static Assets (e.g., style.css, /favicon.ico):

    • The URL stays the same even when the content changes.
    • Strategy: Cache for a short time and require revalidation.
    • Cache-Control: public, no-cache
    • This allows caches to store the file but forces them to check the ETag on every request, ensuring users always get the latest version without re-downloading if it's unchanged.
  • User-Specific API Responses (e.g., /api/me):

    • The content is dynamic and private to one user.
    • Strategy: Only allow the browser to cache, and force revalidation.
    • Cache-Control: private, no-cache
  • Public API Responses (e.g., /api/products):

    • The content is the same for all users but might be updated periodically.
    • Strategy: Allow all caches to store it, but for a short duration.
    • Cache-Control: public, max-age=60
    • This allows a CDN to serve the product list for 60 seconds before needing to ask your server for a fresh copy.

Conclusion

In this lesson, we've armed you with the knowledge to control one of the most powerful performance levers in web development: HTTP caching. By sending the correct headers from your server, you can instruct browsers and other intermediaries on how to store, reuse, and validate responses.

Key Takeaways:

  • HTTP caching reduces latency, server load, and network traffic by storing and reusing responses.
  • The expiration model, primarily controlled by the Cache-Control header, defines how long a response is considered "fresh."
  • Key Cache-Control directives include max-age, public, private, no-cache, and no-store, each serving a distinct purpose.
  • The validation model, primarily using ETag and If-None-Match headers, provides an efficient way to check if a stale resource has changed, avoiding unnecessary data transfer by using a 304 Not Modified response.
  • An effective caching strategy is context-dependent and requires tailoring your headers to the specific resource's volatility and privacy requirements.

Preview of the Next Lesson:

We've now discussed all the individual components of our backend stack in theory: Nginx as a reverse proxy, Gunicorn as a WSGI server, and Django as the application. In the next lesson, we will get practical and configure Nginx as a reverse proxy to forward requests to a Gunicorn-managed Python application. This will be a hands-on session to connect all the pieces we've learned about.