# HTTP 304 Not Modified

**Code:** `304` · **Category:** HTTP · **Seen on:** Nginx, Apache

The cached copy is still valid, so the server sends no body and the client reuses its cache.

## Meaning

304 is a performance win, not an error. It answers conditional requests (`If-None-Match` with an ETag, or `If-Modified-Since`). It becomes a problem when users keep seeing stale CSS/JS/API data, or when code treats 304 as a failure.

## Common causes

- Static assets changed but filenames/ETags didn’t (no cache busting)
- ETags differ across servers behind a load balancer, causing needless 200s
- API clients mishandling empty 304 bodies
- Proxy/CDN caching with long max-age

## Quick fix

1. Version static files (`app.css?v=123` or hashed filenames)
2. Purge the CDN cache after deploys
3. In fetch, use `cache: 'no-store'` for data that must be fresh
4. Treat 304 as success when you send conditional headers yourself

## Detailed fix by platform

### Nginx

1. Long cache for hashed assets, revalidate HTML:

```nginx
location ~* \.(css|js|png|svg|woff2)$ { expires 30d; add_header Cache-Control "public, immutable"; }
location / { add_header Cache-Control "no-cache"; }
```


### Apache

1. Use mod_expires/mod_headers; with multiple servers, `FileETag MTime Size` avoids inode-based ETag mismatches.

## Code examples

### See the conditional request

```bash
curl -sI https://example.com/app.css | grep -i etag
curl -sI -H 'If-None-Match: "65f1-5e2a"' https://example.com/app.css   # → HTTP/2 304
```

## How to diagnose

1. **Freshness** — Is the user actually seeing stale content?
2. **Validators** — ETag / Last-Modified values before and after deploy
3. **Caching layers** — Browser, CDN, proxy — which one served it?

---
Source: [WhatIsMyError](https://whatismyerror.com/errors/http/304-not-modified)
