HTTP 304 Not Modified: Conditional Requests
Intermediate
The HTTP 304 Not Modified response is the backbone of efficient cache revalidation across the web. When a cached resource expires, the client does not blindly re-download the entire resource. Instead, it asks the server a conditional question: "Has this resource changed since I last fetched it?" If the answer is no, the server returns a 304 status with no body — saving bandwidth, reducing latency, and decreasing origin server load. For CDN engineers, understanding 304 responses is essential because cache revalidation is one of the most frequent operations between CDN edge servers and origin servers.
Conditional requests rely on two validation mechanisms: ETags (entity tags) and Last-Modified timestamps. Both serve the same purpose — identifying whether a resource has changed — but they use different approaches with different tradeoffs. The HTTP specification defines how these mechanisms interact and which takes precedence when both are present.
How Do Conditional Requests and 304 Responses Work?
The 304 flow involves two separate HTTP transactions. In the first transaction, the server sends the full resource with validation headers (ETag and/or Last-Modified). In the second transaction — which may occur seconds, hours, or days later — the client uses those stored validation headers to make a conditional request.
The process works in four steps:
- Initial request — the client sends a normal GET request with no conditional headers
- Response with validators — the server returns a 200 response with the full body, plus an
ETagheader and/orLast-Modifiedheader - Conditional request — when the cached response expires (based on
Cache-ControlorExpires), the client sends a new request withIf-None-Match: "etag-value"orIf-Modified-Since: date - 304 or 200 — the server compares the validators. If the resource is unchanged, it returns 304 with no body. If changed, it returns 200 with the new body and new validators
What Are ETags and How Are They Generated?
An ETag (Entity Tag) is a string that uniquely identifies a specific version of a resource. The HTTP specification does not prescribe how ETags should be generated — only that they must change when the resource content changes. Common generation methods include:
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
Content hash — computing an MD5, SHA-1, or CRC32 hash of the response body. This is the most reliable method because it detects any content change, regardless of timing. Nginx generates ETags using a combination of the file's last modification time and content length: ETag: "last_modified_time-content_length".
Inode-based — Apache's default ETag includes the file's inode number, modification time, and size: ETag: "inode-mtime-size". This creates a problem in load-balanced environments because the same file on different servers has different inode numbers, producing different ETags for identical content.
Version string — application-generated ETags based on database record versions, commit hashes, or sequential version numbers. This is common for dynamic content where the application knows exactly when data changes.
Strong vs Weak ETags
The HTTP specification distinguishes between strong and weak ETags:
- Strong ETag (
"abc123") — guarantees byte-for-byte identical representations. Required for range requests (206 Partial Content) and byte-range caching - Weak ETag (
W/"abc123") — guarantees semantically equivalent representations that may differ in insignificant ways (whitespace, formatting, comments). Sufficient for cache validation but cannot be used for range requests
In practice, most ETags are strong. Weak ETags are useful when a resource has multiple valid representations (such as HTML generated from a template where whitespace varies) that should all be considered "unchanged" for caching purposes.
How Does Last-Modified Differ from ETag?
The Last-Modified header contains the date and time when the resource was last changed. The client stores this value and sends it back as If-Modified-Since in conditional requests. The server compares the stored modification time with the resource's current modification time.
# Server response
Last-Modified: Wed, 15 Oct 2025 14:30:00 GMT
# Client conditional request
If-Modified-Since: Wed, 15 Oct 2025 14:30:00 GMT
Last-Modified has a significant limitation: its resolution is 1 second. If a resource changes twice within the same second, the Last-Modified timestamp remains the same, and a conditional request will incorrectly return 304 for a changed resource. This is a real concern for dynamically generated content, build pipelines that update multiple files rapidly, and high-frequency data feeds.
When both ETag and Last-Modified are present, the HTTP specification defines clear precedence: If-None-Match (ETag) takes priority over If-Modified-Since. The server evaluates the ETag first — if it matches, the response is 304 regardless of the Last-Modified value. This means ETags provide a safety net against Last-Modified's 1-second resolution limitation.
Why Is 304 Critical for CDN Cache Revalidation?
CDN cache revalidation is the process where an edge server confirms that its cached content is still valid with the origin server. When a cached object's TTL expires (based on Cache-Control headers), the edge server does not simply delete the cached content. Instead, it sends a conditional request to the origin.
The bandwidth savings from 304 responses are dramatic. Consider a CDN with 200 edge PoPs, each caching a 5MB video file with a 1-hour TTL. Without conditional requests, every PoP would re-download 5MB every hour — 200 × 5MB × 24 hours = 24GB of daily origin bandwidth for a single file. With 304 responses (assuming the video rarely changes), the daily origin bandwidth drops to 200 × 200 bytes × 24 = approximately 1MB — a reduction of over 99.99%.
The stale-while-revalidate directive complements 304 by allowing the CDN to serve slightly stale content while performing revalidation in the background. This eliminates the latency penalty of waiting for the origin's 304 response before serving the user. Combined with conditional requests, stale-while-revalidate provides both freshness guarantees and low-latency responses.
What Common Issues Affect 304 Revalidation?
Clock skew between servers causes incorrect Last-Modified comparisons. If the origin server's clock is ahead of the CDN edge server's clock, conditional requests may fail because the edge server sends an If-Modified-Since timestamp that the origin considers "in the future." Always synchronize server clocks using NTP (Network Time Protocol).
Inconsistent ETags across load-balanced origins is a common problem. If your origin consists of multiple servers behind a load balancer, each server must generate identical ETags for the same content. Apache's default inode-based ETags fail this requirement because identical files on different servers have different inodes. Fix this by configuring Apache to exclude the inode from ETag generation (FileETag MTime Size) or by using content-hash-based ETags.
Missing ETag or Last-Modified headers prevent conditional requests entirely. If the origin server does not include validation headers in its responses, CDN edge servers cannot send conditional requests and must re-download the full resource on every revalidation. Ensure your origin server configuration includes at least one validation header for all cacheable responses.
How Do You Configure ETags and Conditional Responses?
In Nginx, ETags are enabled by default for static files. The etag directive controls this behavior:
# Enable ETags (default)
etag on;
# Nginx generates ETags as: last_modified_hex-content_length_hex
# Example: ETag: "5f3a1b2c-1a4f"
In Apache, configure ETags to exclude inode for load-balanced environments:
# Remove inode from ETag (important for load-balanced origins)
FileETag MTime Size
# Or disable ETags entirely and rely on Last-Modified
FileETag None
Header unset ETag
For dynamic content served by application servers (PHP, Node.js, Python), generate ETags in application code based on content hashes or database record versions. This ensures that ETags accurately reflect content changes rather than file system metadata.