HTTP 304 Not Modified

The resource you requested has not changed since the last time you fetched it. The server is telling your browser to use the cached copy instead of downloading the content again, saving bandwidth and improving page load speed.

What Is HTTP 304 Not Modified?

HTTP 304 Not Modified is a redirection status code used for cache validation. When a client has a cached version of a resource and wants to check if it is still current, it sends a conditional request with either an If-None-Match header containing the cached ETag value or an If-Modified-Since header containing the cached Last-Modified date. If the server determines the resource has not changed, it responds with 304 and no body, telling the client to reuse its cached copy.

This mechanism is fundamental to HTTP caching and dramatically reduces bandwidth usage and page load times across the web. Without conditional requests and 304 responses, every page visit would require downloading the full content again, even if nothing changed. For large resources like images, CSS files, and JavaScript bundles, this savings is substantial. CDNs and reverse proxies also rely heavily on 304 to validate their cached content with origin servers.

The 304 response must not contain a message body. It may include headers that would have been sent with a 200 response, such as Cache-Control, Content-Location, ETag, Expires, and Vary. These updated headers allow the client to refresh its cache metadata without re-downloading the content. Understanding how 304 works is essential for web performance optimization, as proper cache validation can reduce server load and improve Time to First Byte for returning visitors.

Common Causes

ETag Match via If-None-Match

The client sent an If-None-Match header with the ETag value from a previous response. The server compared it to the current ETag and found they match, meaning the content has not changed.

Timestamp Match via If-Modified-Since

The client sent an If-Modified-Since header with the date from a previous response. The server determined the resource has not been modified since that date.

CDN Cache Validation

A CDN or reverse proxy is validating its cached copy with the origin server. The origin confirms the content is still fresh, so the CDN continues serving its cached version.

Browser Cache Revalidation

The browser's cached entry has expired according to its max-age or expires header, so it makes a conditional request to check if the content is still valid before downloading it again.

How to Fix

Force Fresh Content

If you need the latest version regardless of cache, add a cache-busting query parameter like ?v=timestamp or use the Cache-Control: no-cache header to force the server to send the full response.

Set ETag Headers Correctly

Generate ETags based on the content hash of your response. Weak ETags prefixed with W/ allow semantically equivalent responses to match, while strong ETags require byte-for-byte equality.

Configure If-Modified-Since

Set the Last-Modified header on your responses so browsers can make conditional requests. The server compares the modification date and returns 304 if nothing changed.

Debug with DevTools

Open browser DevTools, go to the Network tab, and check the request headers for If-None-Match or If-Modified-Since. Verify the server is correctly returning 304 versus 200 based on cache validity.

Code Examples

Express.js

// Express.js — conditional requests with ETag
const express = require('express');
const crypto = require('crypto');
const app = express();

app.get('/api/config', (req, res) => {
  const data = { version: '2.1.0', features: ['dark-mode', 'export'] };
  const body = JSON.stringify(data);

  // Generate ETag from content hash
  const etag = crypto.createHash('md5').update(body).digest('hex');
  const clientEtag = req.headers['if-none-match'];

  // If client's cached version matches, return 304
  if (clientEtag === etag) {
    return res.status(304).end();
  }

  res.set('ETag', etag);
  res.set('Cache-Control', 'max-age=3600');
  res.json(data);
});

// Express built-in ETag support (enabled by default)
// app.set('etag', 'strong');

Flask (Python)

# Flask — conditional requests with ETag
from flask import Flask, jsonify, request, make_response
import hashlib

app = Flask(__name__)

@app.route('/api/config', methods=['GET'])
def get_config():
    data = {'version': '2.1.0', 'features': ['dark-mode', 'export']}
    body = jsonify(**data).get_data(as_text=True)

    # Generate ETag from content hash
    etag = hashlib.md5(body.encode()).hexdigest()
    client_etag = request.headers.get('If-None-Match')

    # If client's cached version matches, return 304
    if client_etag == etag:
        return '', 304

    response = make_response(jsonify(**data))
    response.headers['ETag'] = etag
    response.headers['Cache-Control'] = 'max-age=3600'
    return response

Frequently Asked Questions

What is the difference between ETag and Last-Modified?

ETag is a content-based identifier that changes when the resource content changes. Last-Modified is a timestamp of when the resource was last changed. ETags are more precise because they detect content changes even when the modification date stays the same, such as when a file is rewritten with identical content.

Does 304 have a response body?

No. A 304 response must not contain a message body. The purpose of 304 is to tell the client to reuse its cached copy, so sending the body again would defeat the purpose. The response may include updated headers like Cache-Control and ETag.

How does 304 affect SEO?

304 responses are positive for SEO because they tell Googlebot that the content has not changed, which saves crawl budget. Googlebot can use conditional requests, and receiving 304 means it does not need to re-parse the content, allowing it to crawl more of your site efficiently.

What is a weak ETag vs a strong ETag?

A weak ETag is prefixed with W/ and indicates semantic equivalence. Two resources with the same weak ETag are functionally identical but may differ in non-significant ways like whitespace. A strong ETag guarantees byte-for-byte identity. Use strong ETags for byte-range requests.

Can I disable 304 responses?

You can send Cache-Control: no-store to prevent caching entirely, which eliminates conditional requests and 304 responses. However, this forces every request to download the full content, increasing bandwidth usage and server load significantly.

Monitor Your APIs & Services

Get instant alerts when your endpoints go down. 60-second checks, free forever.

Start Monitoring Free →