
Different resources need different cache policies
A returning reader should not download unchanged images again. At the same time, retaining old HTML can hide newly published articles. Start with three resource classes: versioned assets, changing documents and private responses. A single cache rule for every route rarely serves all three well.
Cache-Control defines storage and reuse behavior. no-cache permits storage but requires validation before reuse; no-store instructs caches not to store the response. private limits shared-cache storage rather than replacing authentication. Decide how account data should be handled independently of performance tuning. Avoid copying a public-asset policy onto personalized endpoints.
Separate freshness, validation and privacy
Long-lived immutable caching is useful when a content change always produces a new URL. Replacing bytes at the same immutable address breaks that contract. Locally hosted WebP images can follow this approach: download the publisher’s original during preparation, then serve an optimized project asset. Reading the page should require no request to the publisher’s image host.
An ETag identifies a representation for validation. A matching If-None-Match can allow a 304 response without the body. The example hashes a constant in-memory document and demonstrates one exact matching tag. It intentionally omits the complete header grammar, weak comparisons and multiple validators; production servers should use a tested conditional-request implementation.
Compare initial and repeat visits in browser Network tools. Memory cache, disk cache and a 304 response are distinct paths. A 304 still needs a network round trip, whereas a fresh cached asset can be reused locally. Record whether developer tools have disabled caching so another person can reproduce the measurement.
Code example and verification
The notes state the prerequisites and expected outcome. Run the example in a test environment, then adapt it to your project’s data and constraints.
1. Educational Python server; localhost only
from http.server import BaseHTTPRequestHandler, HTTPServer
from hashlib import sha256
BODY = b"Current knowledge article\n"
TAG = '"' + sha256(BODY).hexdigest() + '"'
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
matched = self.headers.get("If-None-Match") == TAG
self.send_response(304 if matched else 200)
self.send_header("Cache-Control", "no-cache")
self.send_header("ETag", TAG)
if not matched:
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(BODY)))
self.end_headers()
if not matched:
self.wfile.write(BODY)
HTTPServer(("127.0.0.1", 8766), Handler).serve_forever()Open http://127.0.0.1:8766 after starting the server. Send its exact ETag in If-None-Match on the next request; expect 304. Stop with Ctrl+C. This is a local teaching server.
2. Separate HTML from versioned assets in Nginx
Classify paths first. The one-year policy applies only to a folder whose filenames change with content, such as app.9c1a2e.css. Keep unversioned files elsewhere and replace root with your test deployment directory.
# Inside a test server {} block:
root /srv/liyan/public;
etag on;
location ~ \.html$ {
add_header Cache-Control "no-cache" always;
}
# Only this folder contains content-hashed, never overwritten assets.
location /assets/versioned/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Unversioned files must not inherit the one-year policy.
location /assets/ {
add_header Cache-Control "public, max-age=3600";
}After nginx -t and a test reload, inspect one HTML response and both asset classes with curl -I. This is a server-block fragment, not a complete configuration. Existing locations and proxies can alter routing. Publish new assets before HTML and retain old assets during the transition.
3. Private responses: choose no-store or private
Choose account-response policy separately from public images. The first response forbids cache retention. The commented alternative permits one minute of browser-only reuse for a different, less-sensitive response. They express different contracts.
# Sensitive account response: do not retain it in HTTP caches.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{"status":"available"}
# A separate, less-sensitive response may permit browser-only reuse:
# Cache-Control: private, max-age=60
# Authentication and authorization still run on the server.This is an HTTP header example, not an authentication implementation. no-store does not erase previously cached responses or by itself control application memory and history. Test signed-out and shared-browser behavior separately.
4. A fuller server: GET, HEAD, ETag and Last-Modified
Stop the introductory server and save this replacement as cache_server.py. It implements body-free HEAD, a 404 for unknown paths and If-None-Match precedence over If-Modified-Since. Weak validators are accepted for GET/HEAD comparison.
from datetime import datetime, timezone
from email.utils import format_datetime, parsedate_to_datetime
from hashlib import sha256
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlsplit
BODY = b"Current knowledge article\n"
TAG = '"' + sha256(BODY).hexdigest() + '"'
MODIFIED = datetime.now(timezone.utc).replace(microsecond=0)
class Handler(BaseHTTPRequestHandler):
def do_HEAD(self):
self.respond(send_body=False)
def do_GET(self):
self.respond(send_body=True)
def respond(self, send_body):
if urlsplit(self.path).path != "/":
self.send_error(404)
return
tags = self.headers.get("If-None-Match")
unchanged = False
if tags is not None:
# Our server emits only quoted hexadecimal validators.
candidates = [tag.strip().removeprefix("W/")
for tag in tags.split(",")]
unchanged = "*" in candidates or TAG in candidates
else:
modified_since = self.headers.get("If-Modified-Since")
if modified_since:
try:
since = parsedate_to_datetime(modified_since)
if since.tzinfo is not None:
unchanged = MODIFIED <= since
except (ValueError, TypeError, OverflowError):
pass
self.send_response(304 if unchanged else 200)
self.send_header("Cache-Control", "no-cache")
self.send_header("ETag", TAG)
self.send_header("Last-Modified", format_datetime(MODIFIED, usegmt=True))
if not unchanged:
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(BODY)))
self.end_headers()
if send_body and not unchanged:
self.wfile.write(BODY)
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8766), Handler).serve_forever()Run python cache_server.py. MODIFIED represents creation of this in-memory representation; use the actual file modification time for file-backed content. This localhost teaching server does not replace production TLS, concurrency, compression or range handling.
5. Inspect the response and validate it manually
On Windows, use curl.exe if curl resolves to another command. Run the second-terminal commands after starting the server, and replace the placeholder with the real validator, including quotes.
# Terminal 1:
python cache_server.py
# Terminal 2: inspect a normal response and its validators.
curl -i http://127.0.0.1:8766/
curl -I http://127.0.0.1:8766/
# Replace the placeholder with the exact quoted ETag from the first response.
curl -i -H 'If-None-Match: "PASTE_THE_HASH_HERE"' http://127.0.0.1:8766/
# A different validator must produce a fresh 200 body.
curl -i -H 'If-None-Match: "different-version"' http://127.0.0.1:8766/Expect a 200 body, a body-free 200 HEAD, a body-free 304 for the matching tag and 200 for a different tag. Compare these paths in browser Network tools too. Disable cache must be off when testing browser reuse.
6. Automated validator and header-precedence checks
Save as check_cache.py and execute it in a second terminal. The final case deliberately combines a mismatched tag with a matching date to verify validator precedence.
# Run while cache_server.py is listening on port 8766.
import urllib.error
import urllib.request
URL = "http://127.0.0.1:8766/"
def request(method="GET", headers=None):
req = urllib.request.Request(URL, headers=headers or {}, method=method)
try:
with urllib.request.urlopen(req, timeout=2) as response:
return response.status, dict(response.headers), response.read()
except urllib.error.HTTPError as response:
return response.code, dict(response.headers), response.read()
status, headers, body = request()
assert status == 200 and body
etag, modified = headers["ETag"], headers["Last-Modified"]
assert request("HEAD")[2] == b""
assert request(headers={"If-None-Match": etag})[0::2] == (304, b"")
assert request(headers={"If-None-Match": "W/" + etag})[0] == 304
assert request(headers={"If-None-Match": "*"})[0] == 304
assert request(headers={"If-Modified-Since": modified})[0] == 304
assert request(headers={
"If-None-Match": '"another-version"',
"If-Modified-Since": modified,
})[0] == 200
print("Cache validation checks passed")Expect Cache validation checks passed. This tests the teaching server’s contract. Repeat the relevant cases through a real CDN or proxy to detect header changes along the delivery path.
7. Vary and representation differences
Add Vary only when the representation at the same URL actually changes with the request header. Separate Persian and English URLs differ from language negotiation at one address. The Python demo performs neither negotiation nor compression.
# If the same URL actually negotiates compressed representations:
Vary: Accept-Encoding
# If the same URL also negotiates its response language:
Vary: Accept-Encoding, Accept-Language
# Separate language URLs such as /article.html and /article-en.html
# do not need Accept-Language variation just because the site is bilingual.
# Emit representation-appropriate validators when response bytes differ.When a proxy generates gzip, test cache behavior and validators for compressed and uncompressed responses. Vary: User-Agent can fragment the cache unnecessarily. Record path, policy, validator, repeat-visit outcome and transferred bytes.
Measure a repeat visit with a clear release strategy
Maintain a route inventory with cache duration and invalidation behavior. During release, ensure HTML references new asset names and do not immediately remove older assets needed by open pages. Judge success by correct content and reduced transfer together. A fast response that keeps readers on stale content fails the product requirement.
Implementation checklist
- Separate HTML and versioned-asset policies.
- Distinguish no-cache from no-store.
- Change immutable URLs whenever bytes change.
- Measure repeat visits with caching settings recorded.
Reference
Editorial guidance: Liyan Knowledge.



