Welcome back. In our previous lesson, we established a robust testing strategy for our HTMX application, ensuring that our server endpoints generate correct and well-structured HTML fragments. With correctness verified, we now shift our focus to an equally critical aspect of production-readiness: performance.
Today's lesson addresses how to make your application faster and more efficient by leveraging one of the web's fundamental performance features: HTTP caching. Your learning outcome is to configure server-side caching headers (ETag, Cache-Control) to optimize responses for HTMX fragment endpoints.
In a hypermedia-driven application, where the client makes frequent, small requests for HTML fragments, effective caching is not just a "nice-to-have"—it's a cornerstone of a responsive user experience. By instructing browsers and intermediate caches how to store and reuse responses, we can significantly reduce latency, minimize server load, and lower bandwidth consumption for your users.
The Fundamentals of HTTP Caching
Before diving into implementation, it's essential to have a clear mental model of how HTTP caching works. At its core, it's a contract between the server and the client, communicated through HTTP headers. The server attaches headers to a response that tell the client (and any caches in between) how long it can reuse that response without asking the server again.
There are two primary caching strategies:
- Expiration: The server specifies a "freshness lifetime" for a resource. The client uses the cached copy until it expires, after which it must fetch a new one. This is controlled by the
Cache-Controlheader. - Validation: The client asks the server if its cached copy is still valid. If the server determines the content hasn't changed, it responds with a special
304 Not Modifiedstatus and an empty body, saving the cost of re-downloading the entire resource. This is typically managed with theETagorLast-Modifiedheaders.
The following video provides an excellent overview of these concepts, covering the different types of caches and the key headers that control them.
Everything you need to know about HTTP Caching
This video from "the roadmap" channel, "Everything you need to know about HTTP Caching," is a great primer. It explains the "why" of caching and systematically introduces the most important headers.
Please watch the following segments: The introduction, which explains the benefits and locations of web caches. The overview of caching headers, which contrasts older headers with the modern Cache-Control header. The detailed explanation of Cache-Control directives. The section on validation, which covers ETag and Last-Modified.
Choosing a Caching Strategy with Cache-Control
The Cache-Control header is your primary tool for defining caching policy. It can contain multiple directives that give you fine-grained control over who can cache a response and for how long. The following flowchart provides a useful decision-making framework.

Let's walk through this flowchart in the context of an HTMX application:
-
Reusable response? If a response contains sensitive, user-specific data that should never be stored (e.g., a one-time password reset link), you would use
no-store. For most HTMX fragments, the answer is "Yes". -
Revalidate each time? For dynamic content that might change between requests (like a list of search results or a user's dashboard), the answer is "Yes". The appropriate directive here is
no-cache. This may sound counter-intuitive, but it doesn't mean "do not cache"; it means "cache, but always check with the server before using." This is the key to enabling validation withETag. -
Cacheable by intermediate caches? If the fragment contains user-specific data (e.g.,
/my-profile), it should beprivate. Only the end-user's browser can store it. If the content is identical for all users (e.g., a product description partial), it can bepublic, allowing CDNs and other shared caches to store it. -
Maximum cache lifetime? For
publiccontent that changes infrequently, you can set amax-age(in seconds) to let caches serve the content without revalidation for a period.
For a deeper dive into these directives and how to implement them in Express, the following article is an excellent resource.
How to Use HTTP Caching Headers in REST APIs - OneUptime
This blog post from OneUptime provides comprehensive examples and best practices for HTTP caching. While it's framed for REST APIs, the principles and Express code apply directly to our HTMX endpoints.
Please focus on two main parts: Read the section Cache-Control Header Options. This covers the key directives with clear explanations and Express.js code samples. Skim the Quick Reference Card for a handy summary of common caching scenarios.
Efficient Validation with ETag
When you use Cache-Control: no-cache, you tell the browser to revalidate its cached copy on every request. This is where the ETag header comes in. An ETag (Entity Tag) is an opaque identifier assigned by the server to a specific version of a resource. Often, it's a hash of the content.
The flow works like this:
- Initial Request: The client requests
/items/1. The server responds with the HTML fragment and anETag: "abcde123"header. The client caches both. - Subsequent Request: The client requests
/items/1again, but this time it includes theIf-None-Match: "abcde123"header. - Server Logic: The server generates the fragment for
/items/1and calculates its ETag.- If the new ETag matches
abcde123, the content hasn't changed. The server sends back a304 Not Modifiedresponse with an empty body. The browser uses its cached version. - If the new ETag does not match, the content has changed. The server sends back a
200 OKresponse with the new HTML fragment and a newETagheader.
- If the new ETag matches
This mechanism is incredibly efficient for HTMX. If a partial hasn't changed, the client avoids re-downloading the HTML, saving bandwidth and reducing perceived latency.
You can inspect these headers in your browser's developer tools. The ETag is a standard response header.

The HTMX-Specific Challenge: Vary: HX-Request
Here is the most critical concept for caching in an HTMX architecture. As we've established, a single URL like /blog might serve two different representations:
- The full HTML page when a user navigates directly to it.
- An HTML fragment when requested by HTMX (indicated by the
HX-Request: trueheader).
A standard cache doesn't know about this distinction. It keys its cache entries by URL. This can lead to a major problem: if an HTMX request for /blog comes first, the cache might store the fragment. When a different user then tries to load the full page, the cache might incorrectly serve them the fragment.
The solution is the Vary HTTP response header. By setting Vary: HX-Request, you instruct all caches that the response for this URL depends on the value of the HX-Request request header. This forces the cache to create separate entries for the full-page version and the HTMX-fragment version, preventing collisions.
The official HTMX documentation highlights this exact point.
The official documentation provides a concise but crucial section on caching that directly addresses the Vary header.
Please read the section titled Caching. Pay special attention to the paragraph explaining the need for Vary: HX-Request when your server renders different content for the same URL.
Implementing Caching in Express for HTMX
Let's synthesize these concepts into a practical Express middleware. We'll create a function that sets the appropriate Cache-Control and Vary headers and then leverages ETags for validation.
We can generate an ETag by hashing the rendered HTML content. Node's built-in crypto module is perfect for this.
Here’s a middleware you could use for a dynamic, user-specific fragment endpoint:
const crypto = require('crypto');
function htmxCache(req, res, next) {
// We will intercept the res.send method to calculate the ETag
const originalSend = res.send;
res.send = function(body) {
if (typeof body === 'string') {
// 1. Set the caching policy
// This is for private, dynamic content. It can be cached by the browser
// but must be revalidated on every request.
res.set('Cache-Control', 'private, no-cache');
// 2. Add the Vary header for HTMX
res.set('Vary', 'HX-Request');
// 3. Generate and set the ETag
const etag = crypto.createHash('sha256').update(body).digest('hex');
res.set('ETag', `"${etag}"`); // ETags must be quoted
// 4. Check for a conditional request
// Express's `req.fresh` utility checks if If-None-Match matches the ETag.
if (req.fresh) {
// If it matches, send a 304 Not Modified and we're done.
return res.status(304).end();
}
}
// If not a conditional request or content is stale, send the full response.
originalSend.apply(res, arguments);
};
next();
}
// Example usage in a route
app.get('/dashboard', htmxCache, (req, res) => {
// This endpoint returns a user-specific dashboard partial.
const user = req.user;
res.render('partials/dashboard', { user });
// Our middleware will intercept the rendered HTML from res.render (which calls res.send)
});
This middleware encapsulates our strategy perfectly:
- It sets a
private, no-cachepolicy, ideal for user-specific dynamic fragments. - It correctly sets
Vary: HX-Request. - It computes a strong
ETagbased on the actual HTML content. - It automatically handles the
304 Not Modifiedresponse for conditional requests, saving significant bandwidth.
You can adapt the Cache-Control header within the middleware for different use cases. For a public, static-like fragment, you might change it to public, max-age=3600. For a form with a CSRF token that should never be cached, you would use no-store.
Conclusion
You now have a complete framework for implementing a sophisticated and effective caching strategy in your HTMX applications. By combining the policy-setting power of Cache-Control with the validation efficiency of ETag and the HTMX-specific necessity of Vary: HX-Request, you can ensure your application is not only correct but also highly performant.
Key Takeaways:
Cache-Controlis for Policy: Use directives likeprivate,public,no-cache, andno-storeto define who can cache a response and how they should treat it.ETagis for Validation: For dynamic content marked withno-cache, use ETags to allow the server to send a lightweight304 Not Modifiedresponse if the content is unchanged.Vary: HX-Requestis Essential: Always use this header for any endpoint that can return both a full HTML page and an HTMX fragment to prevent cache collisions.- Middleware is a Good Pattern: Encapsulating your caching logic in an Express middleware makes it reusable and keeps your route handlers clean.
In our next lesson, we will explore another performance feature, hx-boost, which progressively enhances standard links and form submissions into AJAX requests, reducing full-page loads and creating a smoother navigation experience.
Can't find a good explanation? Sign up and we'll make it for you
Sign up