
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Misconfigured HTTP caching is one of the most common causes of unnecessary backend load and poor user experience I see when auditing infrastructure. Properly implementing Cache-Control, ETag, and Conditional Requests solves this by allowing browsers and CDNs to validate content without re-downloading it, significantly reducing latency and egress costs. This guide provides the exact configuration patterns needed to balance freshness with performance in modern production environments.
How do Cache-Control, ETag, and Conditional Requests work together?
These three mechanisms form a layered defense against unnecessary network traffic. Understanding their distinct roles prevents the common mistake of relying on only one. For teams managing high-traffic applications, especially those hosting high-traffic e-commerce sites where every millisecond of latency impacts conversion, this layering is non-negotiable.
The specific role of each header
- Cache-Control: The primary directive that tells caches (browser, CDN, proxy) how long a response is considered fresh. Directives like
max-age=3600mean "serve from cache for 1 hour without checking the server." This eliminates round-trips entirely during the freshness window. - ETag: A validator token (usually a hash or version string) representing the exact state of the resource. When content changes, the ETag changes. It enables byte-level accuracy that time-based expiration cannot provide.
- Conditional Requests: Headers like
If-None-Matchsent by the client containing the previously received ETag. The server uses this to decide whether to send a full 200 response or a lightweight 304 Not Modified.
A common mistake I encounter in audits is setting long max-age values without ETags. This creates a "stale content trap" where users see outdated assets until the timer expires, even if you deploy a fix immediately. Conversely, using ETags without max-age forces validation on every single request, negating the latency benefits of caching. The combination gives you both instant loads during the freshness window and safe, low-cost validation afterward.
How do you configure Nginx for optimal HTTP caching?
Nginx requires explicit configuration to emit these headers correctly. Default configurations often omit ETags for proxied content or set overly conservative Cache-Control values. Here is a production-tested configuration block for static assets and API responses.
# Static assets - immutable fingerprinted files
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
# Fingerprinted assets can be cached indefinitely
add_header Cache-Control "public, max-age=31536000, immutable";
# Enable ETag generation (on by default for static files)
etag on;
# Ensure gzip doesn't interfere with ETag validation
gzip_proxied any;
gzip_vary on;
}
# HTML documents - always validate
location ~* \.html$ {
add_header Cache-Control "public, max-age=0, must-revalidate";
etag on;
}
# API responses - short TTL with validation
location /api/ {
add_header Cache-Control "private, max-age=60, stale-while-revalidate=300";
# Generate strong ETag from response body
# Note: Requires ngx_http_etag_module (default in modern Nginx)
etag on;
} When configuring Nginx for dynamic content behind a reverse proxy, be aware that etag directives may not automatically apply to proxied responses unless the upstream generates them. In such cases, ensure your application framework emits ETags, or use Nginx's proxy_cache module to generate them at the edge. For deeper infrastructure tuning, refer to the Ubuntu server performance optimization guide to ensure the OS network stack supports your caching throughput.
What is the difference between strong and weak ETags?
Not all ETags are created equal. Choosing the wrong type breaks conditional requests for compressed content or causes false positives during validation. This distinction matters deeply when serving content through CDNs or gzip-enabled proxies.
| Feature | Strong ETag | Weak ETag |
|---|---|---|
| Format | "abc123" | W/"abc123" |
| Byte-for-byte guarantee | Yes — identical content required | No — semantically equivalent is sufficient |
| Works with gzip/brotli? | No — compression changes bytes | Yes — semantic equivalence preserved |
| Use case | Static files, binary downloads, range requests | HTML pages, JSON APIs, dynamically compressed content |
| Nginx default for proxied | Disabled (unsafe with transforms) | Must be explicitly configured |
In practice, I recommend strong ETags for all static assets served directly from disk, and weak ETags for any response that passes through a compression filter or template engine. If you're using Nginx as a reverse proxy with gzip on, strong ETags from the upstream will be stripped because the compressed body no longer matches the original hash. Weak ETags survive this transformation because they assert semantic equivalence rather than byte identity. Always verify your ETag behavior with curl -I -H "Accept-Encoding: gzip" to confirm headers aren't being silently dropped.
How do you debug and validate HTTP caching headers?
Assuming your caching configuration works is a critical operational risk. You must verify that headers are emitted correctly and that conditional requests actually return 304s. Here is my standard validation workflow.
- Initial request check: Run
curl -sI https://example.com/app.jsand verifyCache-Controlcontains expected directives andETagis present. Absence of either indicates a misconfiguration. - Conditional request test: Copy the ETag value and run
curl -sI -H 'If-None-Match: "copied-etag"' https://example.com/app.js. A correct implementation returnsHTTP/2 304with no body. A 200 response means validation failed. - Compression interaction test: Repeat step 2 with
-H 'Accept-Encoding: gzip'. If you get a 200 instead of 304, your strong ETag is incompatible with compression. Switch to weak ETags or disable gzip for that resource type. - CDN verification: Test against your CDN endpoint directly, not just the origin. CDNs may strip or rewrite Cache-Control headers. Check
X-CacheorCF-Cache-Statusheaders to confirm edge behavior matches origin intent. - Browser DevTools audit: Open Network tab, reload the page, and filter by "304". Resources that should validate but show 200 indicate missing If-None-Match headers or server-side validation bugs.
For applications generating ETags in code, ensure the hash input includes all factors that affect output: file content, template variables, user permissions (for private content), and compression algorithm. A frequent bug is hashing only the raw file while serving gzipped variants, causing clients to receive corrupted content after a 304. When troubleshooting complex caching layers across microservices, the observability comparison guide helps identify whether failures originate at the application, proxy, or CDN tier.
Implementing Cache-Control, ETag, and Conditional Requests correctly
Getting Cache-Control, ETag, and Conditional Requests right is a force multiplier for web performance and infrastructure efficiency. Start by auditing your current headers with curl, then apply the immutable/mutable split shown above. Test every change against both compressed and uncompressed variants, and verify 304 responses at the CDN edge, not just the origin. These configurations compound over time — a site serving 1M requests/day can save terabytes of monthly egress and thousands of compute hours with correct caching. If your team needs help designing an audit-ready caching strategy that balances performance with compliance requirements, reach out to discuss your infrastructure.