Skip to content
Language:en Author:Laborci Gergely
reliability

HTTP cache and CDN in practice

Objectives

By the end of the lesson, you can explain the main directives of Cache-Control, the workings of freshness and revalidation (ETag, 304), you can tell a private cache from a shared one, and you know why a badly configured shared cache can be a privacy incident.

Why ask for the same thing again?

A visitor opens the home page: the browser downloads the HTML, the style sheet, the scripts, the logo, and the photos. On the next page the same logo, font, and style sheet appear. It would be wasteful to send these again at every click. And if ten thousand visitors ask for the same 200-kilobyte image, the server and the network do a senselessly large amount of repeated work. A cache preserves the earlier result for later use.

A cache, however, cannot "lie" without limit. Yesterday's news may still be useful, but an old value of an exam date or a stock level can cause harm. Cache design is therefore a trade-off among freshness, speed, cost, and load capacity.

Layers: private and shared cache

MDN distinguishes two kinds:

  • Private cache: tied to one client, typically the browser's. It can also store a personalized response (Cache-Control: private). Very fast, but helps only one user.
  • Shared cache: stands between the client and the server, and serves several users. This includes proxies and managed caches (reverse proxy, CDN, service worker, see 4.4 and 6.6). A personalized response must not be stored here.

A third layer is the server's own (application or data) cache: the result of a costly query that the program keeps in memory for a few minutes. Its fault is often spectacular: if after course registration the old headcount is shown for minutes, that is probably a refresh question, not "bad internet".

flowchart LR
    B[Browser cache: private] --> E[CDN / reverse proxy: shared]
    E --> A[Application cache]
    A --> O[Origin server and database]

Fresh, stale, revalidated

A stored response is fresh while its age is less than its lifetime; afterwards it is stale. The headers communicate the lifetime:

Cache-Control: public, max-age=300

According to MDN max-age=N means that the response is fresh for N seconds. A shared cache communicates in the Age header how long the response has been with it (RFC 9111, see the example of example.com in lesson 4.4: age: 1950). max-age does not mean that the resource cannot change, but that the server accepts: for the given time the earlier version is acceptable.

If there is no Cache-Control, a cache may still store and use a response, based on a heuristic (according to MDN typically up to about 10% of the time since Last-Modified). That is why it is always worth stating the rule explicitly.

The main directives

Directive Meaning (MDN, based on RFC 9111)
max-age=N Fresh for N seconds.
no-cache Can be stored, but must be revalidated with the server before every use. (It does not mean "do not store".)
no-store Must not be stored anywhere. Not even in a private cache; it is not a reliable privacy tool, and it also loses other benefits (for example bfcache).
private Only the individual user's private cache may store it.
public May be stored even if there is an Authorization header in the request; use only when necessary.
immutable The content does not change during its lifetime; a refresh (reload) need not revalidate.

MDN's typical settings:

# Dynamic HTML, should always be fresh
Cache-Control: no-cache

# Personal page of a logged-in user
Cache-Control: no-cache, private

# Sensitive data never to be stored
Cache-Control: no-store

# Versioned static file (for example app.4f8a2c.js)
Cache-Control: public, max-age=31536000, immutable

Revalidation: ETag and 304

A stale response can be revalidated with a conditional request. The server gives an identifier in the ETag header for a particular version of the response. The client can later send it in the If-None-Match header: "I have the version with this ETag; has it changed?" If not, the server sends a 304 Not Modified response, without a body, and the browser can regard the stored copy as fresh again. This way there is a network round trip, but the content need not be downloaded again. Last-Modified / If-Modified-Since do the same with dates.

Tried on the trial server (Cache-Control: no-cache + ETag):

$ curl -i localhost:8092/kurzusok                                 # 1st request
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "c5b2f274e35f"
Cache-Control: no-cache

[{"id":"webprog","szabad":9}]

$ curl -i localhost:8092/kurzusok -H 'If-None-Match: "c5b2f274e35f"'   # 2nd request
HTTP/1.1 304 Not Modified
ETag: "c5b2f274e35f"
Cache-Control: no-cache

The second response has an empty body: the client learned that its copy is still good, without downloading it again.

Versioned URL (cache busting)

According to MDN the HTML of the home page cannot be cached well, because its address does not change; it therefore needs no-cache. Static resources (JavaScript, CSS, images), however, can be cached for a very long time if the file name contains the version or the digest: app.4f8a2c.js. At a new release a new file name (app.91bd77.js) arises, which the HTML already references under the new name, so the old stored copy bothers nobody. The tried header: Cache-Control: public, max-age=31536000, immutable (one year).

The danger of the shared cache: leaking personal data

Imagine that the personalized "My enrolments" page of Kurzuspont gets no private marking, and someone even sets public, max-age=60 "for the sake of speed". A shared cache (a CDN or reverse proxy) stores the response by the URL. We tried a simple shared cache that uses only the URL as the key, with two users:

Origin setting: Cache-Control: public, max-age=60  (WRONG)
  Anna requests : X-Cache: MISS   <p>Üdv, anna! Jelentkezéseid: ...anna adatai...</p>
  Béla requests : X-Cache: HIT    <p>Üdv, anna! Jelentkezéseid: ...anna adatai...</p>   <-- Béla sees Anna's data

Origin setting: Cache-Control: private  (CORRECT)
  Anna requests : X-Cache: MISS   <p>Üdv, anna! ...</p>
  Béla requests : X-Cache: MISS   <p>Üdv, bela! ...</p>

(The page text means "Welcome, anna! Your enrolments: ...anna's data...".)

In the first case Béla got Anna's data, because the shared cache gave the response of the first request to everyone. This is a privacy incident (9.6), whose cause was a single faulty header. MDN also stresses: a personalized response must be marked private (Vary is not a solution for cookies), and shared caches may also collapse identical, simultaneous requests into one request, which makes the situation worse for personalized content.

The Vary header prescribes a separate cache entry according to the values of the given request headers (for example Vary: Accept-Language). MDN does not recommend using Vary: User-Agent, because it creates too many variants.

CDN in practice: the edge cache

The CDN was introduced in lesson 4.4. From the cache's viewpoint, the CDN is a shared cache, which stores at nodes close to the user (the edge). The age of responses (Age) and the hit signal (cf-cache-status: HIT in the response of example.com) show that the response came from the cache. The advantage of a CDN is conditional: it is greatest for static, public, versioned files, and it solves nothing for a personalized or database-heavy response if the slowness arises in the origin application.

Geographic distance matters (4.6), because the signal does not teleport, but a CDN is useful also because it protects the origin server from mass load: during a popular announcement the edge nodes absorb many identical requests. A CDN can then be useful for TLS termination or filtering attacks, but it cannot take over a dynamic business decision (authorization, 7.4) without thought.

Invalidation

MDN's remark: responses already stored in intermediate caches cannot be "recalled". If a faulty file has to be fixed, it is risky to upload a new one to the same URL (some may still see the old one). It is safer to give a new URL (for example tajekoztato-2026-09-v2.pdf), or to use the CDN's invalidation (purge) function, and for a frequently changing resource to give no-cache, so that control stays in the hands of the server.

Worked example: the cache rules of Kurzuspont

Resource Rule Reason
app.4f8a2c.js, stilus.9b1e.css, the logo public, max-age=31536000, immutable Versioned file name: a new URL at a new release.
A regulation in PDF (Portable Document Format, szabalyzat-2026-09.pdf) public, max-age=86400 Does not change after release; a new name on correction.
Course description (public HTML) public, max-age=300 + ETag Changes rarely, a five-minute discrepancy is acceptable.
/kurzusok API (public data) no-cache + ETag We always check, but the 304 is cheap.
"My enrolments" private, no-cache Personal data: must not get into a shared cache.
The payment page and its responses no-store Sensitive, irreversible operation.
Number of free places no cache / a few seconds Live data; the server decides (5.5, 6.5).

The good solution is not "switching everything off", but choosing a rule for the nature of the content.

Common misconceptions

Statement Clarification
"A cache is always faster, so let's store everything." For personal, sensitive, or fast-changing responses old data is dangerous.
"no-cache means do not store it." It means: it may store, but must revalidate before use. The prohibition is no-store.
"If I upload the new file, everyone sees it at once." Only if the rules for old copies, invalidation, or a new URL ensure it.
"A CDN speeds up every page." It does not solve the slowness of a personalized, database-heavy response.
"A cache is only a performance optimization." It is a data-handling rule: it affects correctness and privacy too.

In brief

  • A private cache serves one client, a shared one several users; a personal response may go only into a private one.
  • A response is fresh for the max-age, afterwards it can be revalidated by ETag/If-None-Match; the 304 is a cheap response without a body.
  • no-cache = revalidate before use; no-store = do not store.
  • With a versioned file name a static resource can be cached even for a year.
  • On the tried example a faulty public header gave Anna's data to Béla.

Review questions

  1. What is the difference between no-cache and no-store?
  2. What does 304 Not Modified do, and why is it cheap?
  3. Why is public, max-age=60 dangerous on a personalized page?
  4. How can a static file be cached for a year so that a new release still takes effect at once?

Learning resources

Prerequisites (2)

  • HTTP
  • CDN

Teaches (2)

  • HTTP cache
  • Cache-Control