HTTP cache headers determine how browsers and proxies cache content—if set incorrectly, they slow down loading times and significantly increase server load. In this article, I'll show you how small header errors can affect your caching strategy sabotage and how you can become measurably faster with just a few corrections.
Key points
The following key points help me to quickly check HTTP headers and keep them clean at all times.
- TTL Choose correctly: cache static assets for a very long time, HTML for a short time and in a controlled manner.
- Validation Use: ETag and Last-Modified reduce unnecessary requests.
- Conflicts Avoid: Origin and CDN headers must match.
- Versioning Use: File hashes enable aggressive cache strategies.
- Monitoring Establish: Measure and systematically increase the hit rate.
What HTTP cache headers really control
Cache-Control, Expires, ETag, and Last-Modified determine whether content is fresh, how long it is valid, and when the browser requests it. With max-age I define the lifetime, with public/private the storage location in browsers or shared caches. Directives such as no-store completely prevent storage, no-cache forces revalidation before use. For static files, a validity period of one year is worthwhile, while HTML gets short periods with intelligent revalidation. I also rely on immutable, if files are guaranteed to remain unchanged via hash version.
This control directly affects latency, bandwidth, and server load. An increased HIT rate reduces waiting times and backend work. In addition, I optimize the transfer with HTTP compression, so that fewer bytes need to be transported. Making a clear distinction here reduces the load on CDNs, proxies, and browser caches alike. This is how I ensure smooth Loading times through.
TTL planning in practice
The appropriate TTL is determined by the frequency of changes, risk, and fallback strategy. For assets with file hashes, I set 12 months because I control changes via new file names. For HTML, I focus on content dynamics: home pages or category pages often remain fresh for 1–5 minutes, while detail pages with comments remain fresh for less time. It is important to have a rollback pathIf an error does go live, I need a quick purge (Edge) and a forced revalidation (must-revalidate) for browsers. API responses get short TTLs, but with stale-Directives so that users see responses in the event of an error. I document these profiles per route or file type and anchor them in the build/deploy pipeline so that no „silent“ changes inadvertently undermine the freshness policy.
How misconfigurations sabotage the strategy
Too short TTLs Like max-age=60 seconds for CSS, JS, or images, constant requests destroy the advantages of the cache. A global no-cache in CMS setups slows things down even when large parts of a page are actually stable. If ETag or Last-Modified are missing, the browser reloads files completely instead of checking them intelligently. Superfluous query strings create fragmented Cache keys and significantly reduce the HIT rate. If Origin sends no-cache, the CDN ignores the edge caches, resulting in longer paths and higher server load.
I can see the result in the metrics: more requests, higher CPU time and increasing response times. During traffic peaks, the risk of timeouts increases. At the same time, bandwidth consumption grows without users noticing any benefits. I can quickly identify such patterns by looking at DevTools. I then first turn to Cache control, before I increase server resources.
Recommendations by content type: the appropriate directives
Depending on the content type, I use different Header, so that caches work effectively and users see up-to-date data. The following table shows tried-and-tested profiles that I use in projects.
| Content | Recommended cache control | Validity | Note |
|---|---|---|---|
| JS/CSS/images (versioned) | public, max-age=31536000, immutable | 12 months | Use file name with hash (e.g., app.abc123.js) |
| Font files (woff2) | public, max-age=31536000, immutable | 12 months | Observe CORS if loaded from CDN |
| HTML (public) | public, max-age=300, stale-while-revalidate=86400 | 5 minutes | Short freshness, smooth reloading in the background |
| HTML (personalized) | private, max-age=0, no-cache | rehabilitation | No sharing in shared caches |
| APIs | public, max-age=60–300, stale-if-error=86400 | 1–5 minutes | Error case with stale cushion |
These profiles cover typical sites and help to quickly create consistent Rules It is important to have clear versioning for assets so that long max-age values do not deliver outdated files. HTML remains short-lived and is updated via revalidation. APIs are given short times and a safety net via stale-if-error. This ensures that pages remain accessible even in the event of disruptions. usable.
Caching error codes and redirects correctly
Redirects and error pages deserve their own rules. 301/308 (permanent) can be cached for very long periods in CDNs and browsers; I often set these to days or weeks to avoid redirect chains. 302/307 (Temporary) get short TTLs, otherwise temporary states are „frozen.“ For 404/410, moderate freshness (e.g., minutes to hours) is worthwhile so that bots and users do not constantly query; for frequently changing content, I keep 404 rather short. 5xxI don't cache errors as a rule, but rely on stale-if-error to temporarily deliver working copies. This keeps the platform stable and reduces the re-rendering load for frequently requested but missing paths.
Using validation correctly: ETag and Last-Modified
With ETag and Last-Modified, the browser checks whether a resource really needs to be reloaded. The client sends If-None-Match or If-Modified-Since, and the server ideally responds with 304 instead of 200. This saves me transmission and reduces the Traffic Clear. For static files, Last-Modified is often sufficient; for dynamically generated content, I use ETags. Important: Consistent ETag generation so that caches recognize hits.
I like to combine validation with stale-Directives. stale-while-revalidate keeps pages fast while updating in the background. stale-if-error ensures reliability in the event of backend problems. This keeps the user experience stable and reduces the load on the servers. The following snippets show typical settings that I use.
Header set Cache-Control "public, max-age=31536000, immutable"
/etc/nginx/conf.d/caching.conf location ~* .(css|js|png|jpg|svg|woff2)$ { add_header Cache-Control "public, max-age=31536000, immutable"; }
Advanced directives and details
In addition to max-age, I specifically use s-maxage, to fill edge caches longer than browsers. For example, the CDN can last for 1 hour, while clients revalidate after 5 minutes. must revalidate Forces browsers to check expired copies before use – important for security-related areas. proxy revalidate applies the obligation to shared caches. With no-transform I prevent proxies from changing images or compression without being asked. For broad compatibility, I optionally send a Expires-Date in the future (assets) or past (HTML), even though modern caches primarily observe cache control. For CDN strategies, I separate browser and edge rules: public + max-age for clients, plus s-maxage/surrogate control for the edge. This separation maximizes HIT rates without stale risks on end devices.
Interaction with CDN and edge caches
A CDN respects origin header – Incorrect directives at the origin override global caches. For shared caches, I set public and, if necessary, s-maxage so that edges last longer than browsers. Surrogate control can also provide rules for edge caches. If no-cache is encountered at the origin, the CDN refuses the requested Storage. That's why I deliberately coordinate browser and CDN strategies.
For new projects, I also examine pre-loading strategies. With HTTP/3 Push & Preload I load critical assets early and reduce render blockages. This technique does not replace caching, it complements it. Together with long TTLs for assets, start-up performance improves noticeably. This is how I work on the network rank before the Server even breaks a sweat.
Vary strategy in detail
Vary decides which request headers generate new variants. I keep Vary to a minimum: for HTML, usually Accept-Encoding (compression) and, if necessary, language; for assets, ideally none at all. Too broad a Vary (e.g., User-Agent) destroys the HIT rate. At the same time, ETags the representation-specific Reflect variant: If I deliver gzip or br, the ETags apply per encoding variant and I set Vary: Accept-Encoding. If I use weak ETags (W/), I make sure to generate them consistently, otherwise there will be unnecessary 200s. Fonts or images should generally do without Vary; this keeps keys stable. My principle: First define which variants are technically necessary – only then extend Vary, never the other way around.
Monitoring and diagnostics in DevTools
I always start in the Network tab the browser tools. There I can see whether responses come from the cache, how old they are, and which directives apply. The Age, Cache-Control, and Status columns help with quick checks. A HIT rate below 50% indicates a need for action; target values of 80% and above are realistic. In the case of outliers, I check the associated headers first.
Tools such as PageSpeed and GTmetrix confirmed my local Measurements. I then compare before and after changes to quantify the benefits. If large transfer volumes are added, I consistently activate modern compression. This saves me additional milliseconds. In this way, I back up every tuning with hard numbers.
Automated checks and CI
To prevent rules from being eroded, I embed header checks in the CI. I define target profiles for each path and have them checked randomly against staging in each build. Simple shell checks are often sufficient:
# Example: Expected directives for versioned assets curl -sI https://example.org/static/app.abc123.js | grep -i "cache-control" # Expected short-term and revalidation for HTML
curl -sI https://example.org/ | egrep -i "cache-control|etag|last-modified" # Inspect age headers and cache status (if available) curl -sI https://example.org/styles.css | egrep -i "age|cache-status|x-cache"
In combination with synthetic tests, I plan regular „header audits.“ Findings flow back into infrastructure code. This ensures that Policies Stable – regardless of who last made changes to the CMS, CDN, or server configuration.
Hosting optimization: Page, object, and opcode caching
In addition to browser and CDN caches, I rely on server caches. Page caching delivers finished HTML pages, object caching buffers database results, and OPcache handles PHP bytecode. These layers greatly reduce the load on the backend when headers are set correctly. Only the combination of fast edges, healthy TTLs, and server caches delivers real peak performance. This is how I keep response times stable, even when Traffic increases.
The following market overview shows what I look for in hosting. A strong hit rate, Redis availability, and a good price drive my choice.
| Hosting provider | PageSpeed Score | Redis support | Price (starter) |
|---|---|---|---|
| webhoster.de | 98/100 | Yes | 4,99 € |
| Other1 | 92/100 | Optional | 6,99 € |
| Other2 | 89/100 | No | 5,99 € |
Invalidation and purge strategies
Cache construction is only half the battle – the Invalidation decides on security and agility. For assets, I trigger changes via file hashes so that no purges are necessary. For HTML and APIs, I plan targeted purges: after deployment (critical routes), after publishing (only affected pages), or after feature flags. I like to support edge caches via tags/keys to cache entire Groups to empty instead of hitting paths individually. Where possible, I use „Soft Purge“: content is immediately marked as „stale“ and only revalidated on the next request. This allows me to avoid load peaks caused by simultaneous re-fetches. Organized mapping is important: which events trigger which purge? This logic needs to be versioned in the platform.
Security and data protection: public vs. private
Personalized pages belong in the Private cache of the browser, not in shared caches. That's why I set private, max-age=0, or no-cache for such content. Public HTML pages can be given public with short freshness. If I pay attention to cookies in the request, the content remains cleanly separated. This prevents external users from unintentionally Data see others.
At the same time, I use strict rules for payment and account areas. No-store prevents any sensitive responses from being stored. For the rest of the site, I remain generous so that performance is optimal. This clear separation keeps the platform fast and secure. I document the Profiles, so that everyone involved remains consistent.
Understanding heuristic caching
If Cache-Control and Expires are missing, caches access heuristics back – approximately a percentage of the time since Last-Modified. This leads to results that are difficult to reproduce and varying freshness. I avoid such automatisms by explicitly applying Cache-Control to every relevant route. Where Last-Modified is inaccurate (e.g., with dynamic templates), I prefer ETags. This allows me to actively control freshness and obtain stable metrics across all clients.
Range requests and large files
Play for media and downloads rangeRequests (206 Partial Content) play a role. I activate Accept-Ranges and deliver consistent ETags/Last-Modified so that browsers can reuse parts cleanly. For versioned video segments (HLS/DASH), I set long TTLs; the manifests themselves remain short-lived. Important: Handle If-Range correctly so that partial areas do not lead to outdated mixed states when changes are made. For sensitive content, the following still applies: no storage with no-store, even if Range is involved.
Quickly fix common errors: my playbook
I'll start with a header inventory: Which directives What does Origin deliver and what does the CDN change? I then define TTL profiles for each content type. Versioned assets get one year, HTML gets five minutes plus revalidation. I activate ETag/Last-Modified wherever it makes sense. I then check whether unnecessary Vary or Query parameters are HIT rate Press.
In the next step, I'll take care of network details outside the cache. An incorrect Character set header or missing compression also costs time. Then I measure again: DevTools, synthetic tests, and real-user monitoring if necessary. If the values are correct, I freeze the rules in the config and keep them versioned. This is how the Quality Step by step.
Briefly summarized
With correct HTTP headers I control what is stored where and for how long – saving time and resources. Long TTLs for versioned assets, short times plus revalidation for HTML, and sensible stale directives bring speed and resilience. Clean cache keys, consistent versioning, and clear rules for public/private prevent typical stumbling blocks. Monitoring provides evidence and reveals remaining gaps. Those who proceed in this way raise the Performance noticeable and stable.


