How to check last-modified header
Read the date with curl -s -D - -o /dev/null <url> | grep -i last-modified, then send it back as if-modified-since. A correct server answers 304 with zero bytes. Send a much older date and the same server must answer 200 with the full body.
Why check this
The date validator is the fallback when no ETag exists, and it is the one most often implemented by halves. Run the pair of conditional requests in regression whenever a static file server, a reverse proxy or a CDN rule changes. The failure it catches is a server that answers 304 to every conditional request regardless of the date, so clients keep a copy that was replaced hours ago and no amount of reloading fixes it. A single 304 in a test log is not evidence; the negative case is.
Prerequisites
- curl 7.0 or later. Any build reads headers and sends the conditional header. See the curl manual.
- A URL returning a real body, so
size_downloadseparates a 304 from a 200 without reading the content. - RFC 9110 section 13.1.3 for the
If-Modified-Sincerules, including the exact date format.
Steps
- Step 1.
Read the modification date from the resource. Copy it verbatim, including the day name and the
GMTsuffix.curl -s -D - -o /dev/null 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control' | grep -i '^last-modified'last-modified: Fri, 11 Sep 2026 01:24:18 GMTThe format is fixed by the specification. A date written any other way is ignored by a correct server, which then returns the whole body.
- Step 2.
Send that exact date back and print the status with the number of bytes downloaded.
curl -s -D - -o /dev/null -H 'if-modified-since: Fri, 11 Sep 2026 01:24:18 GMT' -w 'status=%{http_code} bytes=%{size_download}\n' 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control' | grep -i -E 'HTTP/|^content-length|status='HTTP/2 304 status=304 bytes=0A real 304 carries no body and no
content-length.bytes=0is the proof, and the absentcontent-lengthline is the second signal. - Step 3.
Run the negative case. A date long before the last change means the content did move on, so the server owes you the full body.
curl -s -D - -o /dev/null -H 'if-modified-since: Mon, 01 Jan 2001 00:00:00 GMT' -w 'status=%{http_code} bytes=%{size_download}\n' 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control' | grep -i -E 'HTTP/|^content-length|status='HTTP/2 200 content-length: 248013 status=200 bytes=248013Steps 2 and 3 together are the check. One answer of each kind means the server compares dates.
- Step 4.
Read the same two headers from a second service to see how a validator arrives without any freshness directive.
curl -s -D - -o /dev/null 'https://httpbin.org/cache' | grep -i -E '^last-modified|^etag|^cache-control'last-modified: Fri, 11 Sep 2026 19:12:48 GMT etag: 28465909695b40779c546e4a42e2e6acNo
cache-controlline came back, so nothing states a lifetime. The date is the only thing a cache can work from. - Step 5.
Run the negative case against that service. The date on the wire is from 2026, so a 2001 date must produce a 200.
curl -s -D - -o /dev/null -H 'if-modified-since: Mon, 01 Jan 2001 00:00:00 GMT' -w 'status=%{http_code} bytes=%{size_download}\n' 'https://httpbin.org/cache' | grep -i -E 'HTTP/|status='HTTP/2 304 status=304 bytes=0This is the broken shape. The server answers 304 because the request header is present, without comparing it to anything. A client obeying that answer keeps stale content forever.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Step 2 304, step 3 200 | Dates are compared correctly | Nothing. Record bytes=0 as the saving per repeat request. |
| Step 2 200 with a full body | The conditional header is ignored or stripped | Check the proxy layer first. Many strip request headers they do not recognise. |
| Step 3 304, as in step 5 | The server answers on header presence alone | Treat every 304 from this service as unverified and raise it as a correctness bug. |
| A 304 with a content-length above zero | The status and the body disagree | Clients differ on what to do. Fix the server; a 304 has no body. |
| No last-modified header | Only an ETag, or no validator at all | Fall back to the ETag check. With neither, every expiry is a full download. |
| last-modified newer than date | The clock on the origin runs ahead | Caches may treat the entry as never fresh. Fix time sync before chasing cache bugs. |
Common mistakes
What to check next
- How to check ETag header: the content-based validator that survives a second-resolution clash.
- How to check cache-control header: without a lifetime, a client may never keep the copy that makes a 304 useful.
- How to check if CDN cache is hit or miss: the edge often answers the conditional request itself, so the origin sees nothing.
- How to check HTTP response headers with curl: the printing and filtering flags behind every command above.
FAQ
How to test 304 not modified response?
Send the validator back and read two numbers: the status and the downloaded byte count. -w 'status=%{http_code} bytes=%{size_download}\n' prints both. 304 with bytes=0 is a real one; anything with a body is not.
How to test conditional requests with if-none-match?
The shape is the same, with the ETag instead of the date. Read etag, send it in if-none-match, then repeat with a wrong value and confirm a 200. The ETag procedure below covers the weak and strong forms.
Which wins when a client sends both headers?
The ETag. A server receiving if-none-match and if-modified-since evaluates the tag and ignores the date. Sending both is still worth doing, because caches that store no tag rely on the date.
Why is a 304 faster when the body is identical anyway?
Because the body never crosses the network. Step 3 moves 248013 bytes and step 2 moves none, for the same content. On a metered or high-latency link that difference is the whole reason the validator exists.
Verified
Verified by Maks Vernycurl 8.21.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic5 minpublished updated Maks Verny