HTTP Caching
An HTTP cache keeps a copy of a response so that a later request for the same resource can be answered without a full trip to the origin server, the server that owns the resource. Your browser has a private cache; shared caches such as a proxy or a CDN sit between many users and the server. RFC 9111 defines the rules, and the server sets them with response headers.
Example: a product page calls GET /api/products/1042, and the response carries Cache-Control: max-age=60 and ETag: "p1042-v7". For 60 seconds the browser reuses its copy without asking. After that it asks whether p1042-v7 is still current; if it is, the server answers 304 Not Modified with no body.
Why it matters
Caching makes repeat visits fast and takes load off the servers, but every cached copy can be out of date or reach the wrong person. Typical defects: an old price or an old script after a release, a page in the wrong language, and the worst kind, a shared cache that serves one customer's account page to the next customer.
Test environments often lack a CDN, and testers often work with DevTools' "Disable cache" switched on, so these defects surface in production unless someone reads the caching headers before release.
Fresh or stale: what Cache-Control decides
A cache stores a response under a cache key made of at least the request method and the URL; most stored responses are successful GET responses. While a stored response is fresh, the cache may reuse it without contacting the server. Once it is stale, the cache normally has to check with the server first.
The freshness lifetime comes from max-age, or else from the older Expires header, which holds a fixed date and is ignored when max-age is present. With neither, a cache may guess a lifetime (heuristic freshness), often from Last-Modified. So a response without caching headers is not "uncached", only unpredictable.
max-age=600: fresh for 600 seconds.s-maxagesets the lifetime for shared caches only.no-cache: may be stored, but must be revalidated with the server before every reuse. Despite its name, it does not forbid caching.no-store: no cache may store the request or the response. This is the directive for personal and sensitive data.private: only the user's own (private) cache may store it.public: any cache may, even where it otherwise would not.must-revalidate: once stale, the copy must not be used without a successful check, even if the server is unreachable.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
ETag: "p1042-v7"
Last-Modified: Wed, 30 Sep 2026 08:00:00 GMT
Date: Thu, 01 Oct 2026 09:00:00 GMTRevalidation: ETag, Last-Modified and 304
To check a stale copy cheaply, the server labels each response with a validator: ETag, an opaque version tag such as "p1042-v7", or Last-Modified, a date. The cache sends it back in a conditional request: If-None-Match for the ETag, If-Modified-Since for the date. When both are present, RFC 9110 tells the server to ignore If-Modified-Since.
If nothing changed, the server answers 304 Not Modified: no body, only headers such as ETag, Cache-Control and Date, which refresh the stored copy that the client then shows. If the resource changed, it answers 200 OK with the new body and a new ETag. An ETag starting with W/ is weak: it promises equivalent content, not byte-for-byte identical content.
If-None-Match: if the ETag still matches, the server answers 304 Not Modified with no body; if the product changed, it sends 200 OK with the new body and a new ETag.Product unchanged
- Browser private cache
- Origin server
shop.example
- 1 From Browser To Origin server: First request
GET /api/products/1042 - 2 From Origin server To Browser: 200 OK with the body and a validator reply
Cache-Control: max-age=60ETag: "p1042-v7" - 3 Browser internally: Reuses the stored copy without asking Fresh for 60 seconds: no request is sent
- 4 From Browser To Origin server: Copy is stale: conditional request
If-None-Match: "p1042-v7" - 5 Origin server internally: Compares the ETag: still current
- 6 From Origin server To Browser: 304 Not Modified, no body reply New
DateandCache-Controlrefresh the stored copy Pass
Outcome: The browser shows the body it already has, fresh for another 60 seconds.
Product changed
- Browser private cache
- Origin server
shop.example
- 1 From Browser To Origin server: Copy is stale: conditional request
If-None-Match: "p1042-v7" - 2 Origin server internally: Compares the ETag: the price changed The current version is
p1042-v8 - 3 From Origin server To Browser: 200 OK with the new body reply
ETag: "p1042-v8" - 4 Browser internally: Replaces the stored copy
Outcome: The new price is shown, and the next conditional request carries
"p1042-v8".
GET /api/products/1042 HTTP/1.1
Host: shop.example
If-None-Match: "p1042-v7"
HTTP/1.1 304 Not Modified
Cache-Control: max-age=60
ETag: "p1042-v7"
Date: Thu, 01 Oct 2026 09:01:05 GMTWorked example: testing a product page behind a CDN
A store serves its product pages through a CDN. Rules: a new price must show within one minute, and account pages must never be cached. Turn the rules into expected headers for each type of response, then test the behaviour with repeatable requests (curl, the API tools or the DevTools Network panel), noting how each page was loaded: link, reload or hard reload.
- Versioned file such as
app.3f9c1a.js: a longmax-ageis correct, because every release gets a new file name. - Product API with
max-age=60and anETag: repeat the request withIf-None-Matchand expect304with no body. Change the price, wait 60 seconds, repeat: expect200, the new price and a new ETag. - Through the CDN: send the same request twice and check that
Ageappears and grows. Then send it withAccept-Language: arand withenand compare the bodies. - Account pages: expect
no-store. Open the same orders URL as customer A and as customer B in two browsers; B must never see A's orders. After logout, Back must not show private data; test this in each target browser, because RFC 9111 does not require the browser history to follow caching rules.
Common mistakes
- Reading
no-cacheas "do not cache"; it means "revalidate before reuse", andno-storeis what forbids storing. - Assuming a response without caching headers is never cached; caches may guess a lifetime.
- Testing only with "Disable cache" switched on, or only where there is no CDN, and concluding that caching works.
- Checking that a 304 arrives but not that the page then shows current content.
- Testing personalised pages with a single account; leaks between users need two.
- Forgetting
Varywhen the response depends on the language or another request header.
The QA perspective
Caching is configured in code, in the web server and in the CDN, often by different people. During refinement, ask how fresh each kind of data must be and record testable answers, such as "prices: at most 60 seconds old; account pages: never stored". These are your expected results.
In a caching bug report, include the full request and response headers, which cache answered (with its Age or hit-or-miss header), the time of each request and how the page was loaded; otherwise a developer who reloads with an empty cache will not reproduce it. Retest caching after every CDN configuration change.
Practice exercise
Review the caching headers of an online store
The five exchanges below come from a store behind a CDN. The rules: prices may be at most 60 seconds old, no cache may store account data, and product data comes in the language requested with Accept-Language. Response 2 is the product page that shows the price; in exchange 5 the product has not changed.
Decide whether each response is cached correctly, and describe every problem you find.
1) GET /assets/app.3f9c1a.js
-> 200 OK
Cache-Control: public, max-age=31536000
2) GET /products/1042
-> 200 OK
Content-Type: text/html
Cache-Control: public, max-age=86400
3) GET /api/account/orders
Authorization: Bearer <access token>
-> 200 OK
Cache-Control: public, max-age=300
4) GET /api/products/1042
Accept-Language: ar
-> 200 OK
Content-Language: ar
Cache-Control: max-age=60
ETag: "p1042-v7"
Vary: Accept-Encoding
5) GET /api/products/1042
If-None-Match: "p1042-v7"
-> 200 OK
ETag: "p1042-v7"
Content-Length: 5120- For each response, write who may store it (browser, shared caches, nobody) and for how long.
- Compare that with the content and the rules.
- For each problem, write the risk, the headers you expect and one test that goes through the real caches.
Show the model answer
Response 1 is correct: the version is part of the file name, so a release changes the URL and a one-year max-age cannot serve old code.
Response 2 is a defect: any cache may show a price up to 24 hours old. Expected: max-age=60, or no-cache with an ETag. Test: change a price and open the page through the CDN 61 seconds later.
Response 3 is a high-severity security defect: public lets a shared cache reuse a response to a request with Authorization, so the CDN may show one customer's orders to another. Expected: no-store. Test: two accounts, the same URL, through the CDN; each must see only their own orders.
Response 4 is a defect: the body depends on Accept-Language, which Vary does not list, so the CDN can serve the Arabic body to an English request. Expected: Vary: Accept-Encoding, Accept-Language. Test: request in Arabic, then in English, and compare Content-Language.
Response 5 is a defect: the ETag matches, so the answer should be 304 Not Modified with no body. Resending 5,120 bytes wastes bandwidth without showing wrong data, so it is less severe than 3. The response also lacks Cache-Control, so caches may guess its lifetime. Expected: 304 with the ETag, Cache-Control: max-age=60 and the same Vary as the fixed response 4. Test: repeat the request and expect 304.
Frequently asked questions
What is the difference between no-cache and no-store?
no-cache lets a cache store the response but requires it to revalidate with the server before every reuse, so the user always gets current content, often through a cheap 304. no-store forbids storing the request and the response at all. Use no-store for personal or sensitive data.
Should a server send ETag or Last-Modified?
Both work as validators and servers often send both, but ETag is more precise: Last-Modified has a resolution of one second and can change when a file is saved again with the same content. When a request carries both conditions, the server uses If-None-Match and ignores If-Modified-Since.
How do I test caching when the test environment has no CDN?
Check the headers there, because the application sets them. Test shared-cache behaviour (Age, hit or miss, Vary, leaks between users) in an environment with the same CDN or proxy configuration as production, such as staging. If there is none, record the gap in your test plan.
Why does the browser send no request at all for some files?
Because they are fresh: within max-age the browser reuses its copy without contacting the server, and the Network panel shows them as served from the cache. A conditional request is sent when the copy is stale or marked no-cache, and a reload can also trigger one, depending on the browser.
References
- RFC 9111: HTTP CachingHow Cache-Control, Expires, ETag and validators decide whether a response may be stored and reused.
- RFC 9110: HTTP SemanticsThe meaning of HTTP methods, header fields, status codes and content negotiation, independent of the wire version.
- HTTP caching — MDNPrivate vs. shared caches, freshness, validation with ETag / Last-Modified and the common Cache-Control patterns.
- OWASP Cheat Sheet Series: Session ManagementSession identifier entropy, cookie attributes, expiry, renewal after login and logout handling.
Practise with Mutqan tools
Analyze Cache-Control, Expires, ETag, Last-Modified, Vary and Age headers and explain how browsers and CDNs will cache the response.
Parse raw HTTP request/response headers into a structured table with explanations.
Compose HTTP headers with presets for caching, CORS, security and content negotiation.
Validate API response headers for correctness, security and caching best practices.
Build cURL commands from method, URL, headers, body and auth options.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.