Cache-Control, ETag, and Conditional Requests

Khimananda Oli 7 min read Database
Cache-Control, ETag, and Conditional Requests

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.

Client / BrowserHolds Cached Copy + ETagOrigin ServerGenerates ETag / ValidatesCDN / ProxyRespects Cache-ControlGET + If-None-MatchForward ValidationDecision Logic1. Client sends request with If-None-Match: "abc123"2. Server compares ETag. If match → 304 Not Modified (0 bytes body)3. If mismatch → 200 OK + New Body + New ETag4. Cache-Control max-age determines if validation is even neededResult: Up to 90% bandwidth savings on static assets
HTTP caching lifecycle demonstrating how Cache-Control, ETag, and Conditional Requests interact to prevent redundant data transfer

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=3600 mean "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-Match sent 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.

Incoming RequestIs asset fingerprinted?YESNOImmutable Strategymax-age=31536000, immutableETag optional (filename = version)Mutable Strategymax-age=0 OR short TTLETag REQUIRED for validationBrowser caches foreverBrowser validates viaIf-None-Match + ETagBoth paths reduce origin load significantly
Decision flowchart for selecting the correct Cache-Control and ETag strategy based on asset mutability

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.

FeatureStrong ETagWeak ETag
Format"abc123"W/"abc123"
Byte-for-byte guaranteeYes — identical content requiredNo — semantically equivalent is sufficient
Works with gzip/brotli?No — compression changes bytesYes — semantic equivalence preserved
Use caseStatic files, binary downloads, range requestsHTML pages, JSON APIs, dynamically compressed content
Nginx default for proxiedDisabled (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.

  1. Initial request check: Run curl -sI https://example.com/app.js and verify Cache-Control contains expected directives and ETag is present. Absence of either indicates a misconfiguration.
  2. 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 returns HTTP/2 304 with no body. A 200 response means validation failed.
  3. 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.
  4. CDN verification: Test against your CDN endpoint directly, not just the origin. CDNs may strip or rewrite Cache-Control headers. Check X-Cache or CF-Cache-Status headers to confirm edge behavior matches origin intent.
  5. 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.

❌ Without Proper CachingEvery request → Full 200 OK + BodyBandwidth: 100% per requestLatency: Full TLS + Transfer + ParseServer CPU: High (repeated generation)User Experience: Slow repeat visitsCost: $$$ Egress + Compute✅ With Cache-Control + ETagFresh: Serve from cache (0 network)Stale: Validate → 304 (~200 bytes)Bandwidth: <5% on repeat viewsLatency: Near-zero for cached assetsServer CPU: Minimal (validation only)Cost: ¢ Pennies per 1K requests
Side-by-side comparison showing operational and cost impact of correct versus missing HTTP caching headers

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.

Frequently Asked Questions

Cache-Control tells browsers how long to store a response locally without checking the server. ETag is a validator token used during conditional requests to confirm if a cached copy remains valid after its freshness lifetime expires.

Add add_header Cache-Control "public, max-age=31536000" inside your location block for static assets. Use private or no-store for dynamic PHP responses. Reload Nginx with nginx -t && systemctl reload nginx to apply changes safely in production environments.

Prefer ETag for precise byte-level validation, especially when file timestamps change without content modification. Last-Modified works well for static files but lacks granularity. Many servers send both; clients prioritize ETag during conditional GET requests per HTTP specifications.

No.

Conditional requests return 304 Not Modified instead of full payloads when content hasn't changed. This eliminates redundant data transfer, reducing egress fees on cloud providers like AWS or Cloudflare while maintaining cache accuracy through ETag or Last-Modified validators.

Yes.

Set private, max-age=0, must-revalidate for authenticated API endpoints to prevent sensitive data caching. Use public, max-age=60 for read-only public resources. Configure via middleware or response headers in Laravel 12 controllers to enforce consistent browser caching policies.

Browsers bypass caching when DevTools network panel has disable cache enabled, or when responses include conflicting Pragma or Expires headers. Verify actual behavior using curl -I to inspect raw headers independent of browser developer tooling or extension interference.

CDNs forward conditional requests to origin servers unless configured to validate at edge. Cloudflare and Fastly can cache ETags and respond with 304 directly if content hash matches, reducing origin load. Enable tiered caching to optimize validation performance across global PoPs.

Browsers serve cached content within max-age without contacting the server. After expiration, they send If-None-Match with the stored ETag. The server responds 304 if unchanged or 200 with new content, combining time-based efficiency with accurate revalidation.

Use curl -H "If-None-Match: \"your-etag\"" -v https://example.com/resource to simulate revalidation. Check for 304 status and empty body. Repeat with modified ETag values to verify server returns 200 when content actually differs from cached version.

No.

Vary tells caches to store separate responses based on specified request headers like Accept-Encoding or Authorization. Each variant gets its own ETag. Omitting Vary causes incorrect cache hits across different user contexts or content negotiations during conditional requests.

Generating weak ETags inconsistently, omitting Vary for personalized content, setting max-age too high without revalidation fallback, or stripping ETag headers at reverse proxies. Audit header chains from application through CDN to ensure validators propagate correctly end-to-end.

No.