HTTP 304 Not Modified: Conditional Requests

· Alex Petrov · 8 min read

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.

HTTP 304 conditional request flow showing initial fetch with ETag, then revalidation returning 304 Not Modified Client Server Step 1: Initial Request GET /style.css 200 OK + ETag: "a1b2c3" + body (45KB) Cache stores: ETag="a1b2c3" Step 2: Revalidation (cache expired) GET /style.css + If-None-Match: "a1b2c3" 304 Not Modified (headers only, ~200 bytes) Client uses cached body — saved 45KB transfer

The process works in four steps:

  1. Initial request — the client sends a normal GET request with no conditional headers
  2. Response with validators — the server returns a 200 response with the full body, plus an ETag header and/or Last-Modified header
  3. Conditional request — when the cached response expires (based on Cache-Control or Expires), the client sends a new request with If-None-Match: "etag-value" or If-Modified-Since: date
  4. 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:

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.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:

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.

Bandwidth comparison between full re-download and 304 conditional response for CDN cache revalidation Bandwidth: Full Download vs 304 Revalidation Without 304: 5MB download With 304: ~200 bytes (headers only) 99.99% bandwidth saved Per revalidation, per PoP Multiplied across 200+ PoPs and thousands of objects

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.

External References

References & Standards: OWASP CDN Security · RFC 7234