← HTTP/HTTPS: applications and diagnosis
09 / 12 · 60 MIN

Real caching: keys, variants and storage controls

Run an NGINX lab and distinguish resource identity, variants and read and storage decisions.

Prepare and predict before execution

Save the complete code below as run.py in a working directory. You need Python 3.13 or later and an NGINX executable with HTTP proxy and cache modules. Set DR_NGINX_BIN to that executable path and run python3 run.py --output evidence.json. The script creates temporary local ports, a fictional origin and an isolated cache. Before execution, write your prediction for each group: origin requests, cache states and received body. Do not reuse this configuration as a production portal configuration. X-Lab headers are teaching controls defined only for this experiment.

Separate connectivity from reuse

In the first group, two GETs to /fresh produce MISS and HIT. The origin receives only one request and the bodies match. This combination explains where reuse occurred. A client without a body cache actually sends both requests to the proxy, so do not attribute the second result to a browser. Record the specific path and count before generalizing. A HIT also does not establish that business data remains suitable for its consuming process. In this experiment the body is a synthetic string, without database dependencies or authorization rules.

Build a representation matrix

The /query group uses fund=A and fund=B, followed by a repeat of A. Expect two origin visits and reuse of representation A on the third request. Then compare pt, en, pt and en on /variant, where the origin sends Vary: Accept-Language. Bodies should follow the language on every read. The worksheet asks two separate questions: was the correct resource identified and was the correct variant selected? In a fictional funds portal, an optimization that loses the fund parameter can deliver the wrong fund data despite excellent latency. Define functional outcomes before measuring timing improvement.

Investigate responses that are not retained

The /nostore, /private and /cookie groups make two identical requests to each endpoint. In this configuration, they all reach the origin and produce MISS. Use recorded headers and effective configuration to explain each outcome instead of starting by clearing directories or increasing disk capacity. The origin may communicate legitimate reuse boundaries. For a fictional low hit-ratio incident, write a hypothesis, the observation distinguishing it from a fault and the change that requires a contract review. Removing personalization signals merely to raise a metric can introduce a more serious isolation problem.

Reading, storing and invalidating are separate decisions

On /bypass, first populate the cache with v1. The origin changes to v2 and a request with X-Lab-Bypass obtains the new version; the next ordinary read produces HIT v2. The origin then changes to v3. X-Lab-Nostore alone still allows HIT v2. With both headers, the client receives v3, but a later ordinary read remains on v2. Draw three columns: where did the body come from, did the request store a response and did the previous entry disappear? This sequence prevents announcing a cache clearance merely because diagnostics could read the origin. The script does not execute a purge API.

Deliver a reproducible conclusion

This review record uses NGINX 1.30.5 and Python 3.13.1. The evidence file contains configuration, origin requests, client responses, per-hop logs and script and binary hashes. Compare the ten group outcomes with predictions and explain one incorrect prediction. Do not expect ports or timestamps to match between runs. The NGINX process exits and its temporary directory is removed at the end; confirm those fields. Execution involves no TLS, browser, CDN, authentication or concurrent load. Finish with a handover note stating what was observed, in which environment and which hypothesis still needs testing.

"""Original DR cache experiment. Only synthetic data and IPv4 loopback.
Usage: DR_NGINX_BIN=/path/to/nginx python3 run.py --output evidence.json
Requires Python 3.13+ and NGINX with HTTP proxy/cache modules; no installation.
"""
import argparse, hashlib, http.client, http.server, json, os, pathlib, platform
import signal, socket, subprocess, tempfile, threading, time
from datetime import datetime, timezone


def run(binary):
 events, checks, responses = [], {}, []
 state = {'version': 'v1', 'outage': False}
 class Origin(http.server.BaseHTTPRequestHandler):
 protocol_version = 'HTTP/1.1'
 def log_message(self, *args):
 pass
 def do_GET(self):
 route = self.path.split('?')[0]
 events.append({'path': self.path, 'language': self.headers.get('Accept-Language'),
 'ifNoneMatch': self.headers.get('If-None-Match')})
 status, headers, body = 200, {'Cache-Control': 'max-age=60'}, self.path
 if route == '/variant':
 headers['Vary'] = 'Accept-Language'
 body = 'language:' + self.headers.get('Accept-Language', 'none')
 elif route == '/nostore':
 headers['Cache-Control'] = 'no-store'
 elif route == '/private':
 headers['Cache-Control'] = 'private, max-age=60'
 elif route == '/cookie':
 headers['Set-Cookie'] = 'synthetic=1; Path=/'
 elif route == '/bypass':
 body = 'version:' + state['version']
 elif route == '/revalidate':
 headers.update({'ETag': '"fixture-v1"', 'Cache-Control': 'max-age=1'})
 body = 'validated-body'
 if self.headers.get('If-None-Match') == '"fixture-v1"':
 status, body = 304, ''
 headers['Cache-Control'] = 'max-age=60'
 elif route in ('/stale', '/strict'):
 headers['Cache-Control'] = 'max-age=1'
 body = 'last-known-value'
 if state['outage']:
 status, body = 503, 'origin-unavailable'
 headers['Cache-Control'] = 'no-store'
 payload = body.encode('utf-8')
 self.send_response(status)
 for key, value in headers.items:
 self.send_header(key, value)
 if status!= 304:
 self.send_header('Content-Length', str(len(payload)))
 self.send_header('Connection', 'close')
 self.close_connection = True
 self.end_headers
 if payload:
 self.wfile.write(payload)
 origin = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Origin)
 origin.daemon_threads = True
 thread = threading.Thread(target=origin.serve_forever, daemon=True)
 thread.start
 # Reserve an ephemeral candidate. NGINX binding failure is fatal, never take over an existing service.
 with socket.socket as s:
 s.bind(('127.0.0.1', 0))
 port = s.getsockname[1]
 process = None
 result = {}
 def check(name, details, valid):
 checks[name] = {'passed': bool(valid), **details}
 if not valid:
 raise AssertionError(name + ': ' + json.dumps(details))
 try:
 with tempfile.TemporaryDirectory(prefix='dr-http-cache-') as tmp:
 root = pathlib.Path(tmp)
 config = '''daemon off;
master_process on;
worker_processes 1;
error_log "ROOT/error.log" notice;
pid "ROOT/nginx.pid"
events { worker_connections 64; }
http {
 access_log "ROOT/access.log" evidence;
 log_format evidence escape=json '{"path":"$request_uri","status":"$status","upstream":"$upstream_status","cache":"$upstream_cache_status"}'
 proxy_temp_path "ROOT/proxy-temp"
 proxy_cache_path "ROOT/cache" keys_zone=lab:1m max_size=10m inactive=2m;
 server {
 listen 127.0.0.1:PORT;
 proxy_cache lab;
 proxy_cache_key "$scheme$proxy_host$request_uri"
 proxy_cache_valid 200 60s;
 proxy_cache_revalidate on;
 proxy_cache_bypass $http_x_lab_bypass;
 proxy_no_cache $http_x_lab_nostore;
 add_header X-DR-Cache $upstream_cache_status always;
 location / { proxy_pass http://127.0.0.1:ORIGIN; }
 location = /stale {
 proxy_cache_use_stale error timeout http_503;
 proxy_pass http://127.0.0.1:ORIGIN;
 }
 }
}
'''.replace('ROOT', str(root)).replace('PORT', str(port)).replace('ORIGIN', str(origin.server_port))
 # log format must be declared before the access_log that names it.
 lines = config.splitlines
 ai = next(i for i,l in enumerate(lines) if 'access_log ' in l)
 lines[ai], lines[ai+1] = lines[ai+1], lines[ai]
 config = '\n'.join(lines) + '\n'
 conf = root/'nginx.conf'
 conf.write_text(config)
 subprocess.run([binary, '-t', '-p', str(root)+'/', '-c', str(conf)], check=True, capture_output=True, text=True)
 output = open(root/'process.log', 'w')
 try:
 process = subprocess.Popen([binary, '-p', str(root)+'/', '-c', str(conf)], stdout=output, stderr=output)
 for _ in range(100):
 if process.poll is not None:
 raise RuntimeError((root/'process.log').read_text)
 try:
 with socket.create_connection(('127.0.0.1', port), timeout=.2):
 break
 except OSError:
 time.sleep(.05)
 else:
 raise TimeoutError('NGINX readiness timeout')
 def get(path, headers=None):
 client = http.client.HTTPConnection('127.0.0.1', port, timeout=5)
 try:
 client.request('GET', path, headers=headers or {})
 r = client.getresponse
 row = {'path':path, 'status':r.status, 'cache':r.getheader('X-DR-Cache'), 'body':r.read.decode}
 responses.append(row)
 return row
 finally:
 client.close
 def count(route):
 return sum(e['path'].split('?')[0] == route for e in events)
 a,b = get('/fresh'),get('/fresh')
 check('fresh-response-reused-without-origin', {'states':[a['cache'],b['cache']], 'originRequests':count('/fresh')}, [a['cache'],b['cache']]==['MISS','HIT'] and a['body']==b['body'] and count('/fresh')==1)
 a,b,c = get('/query?fund=A'),get('/query?fund=B'),get('/query?fund=A')
 check('query-string-separates-cache-keys', {'states':[x['cache'] for x in (a,b,c)],'originRequests':count('/query'),'bodies':[x['body'] for x in (a,b,c)]}, a['body']!=b['body'] and a['body']==c['body'] and count('/query')==2 and c['cache']=='HIT')
 rows = [get('/variant', {'Accept-Language':l}) for l in ['pt','en','pt','en']]
 check('vary-separates-language-representations', {'states':[r['cache'] for r in rows],'bodies':[r['body'] for r in rows],'originRequests':count('/variant')}, [r['cache'] for r in rows]==['MISS','MISS','HIT','HIT'] and [r['body'] for r in rows]==['language:pt','language:en','language:pt','language:en'] and count('/variant')==2)
 for route,label in [('nostore','no-store-not-retained'),('private','private-not-shared'),('cookie','set-cookie-not-cached-by-this-configuration')]:
 rows = [get('/'+route),get('/'+route)]
 check(label, {'states':[r['cache'] for r in rows],'originRequests':count('/'+route)}, [r['cache'] for r in rows]==['MISS','MISS'] and count('/'+route)==2)
 get('/bypass');state['version']='v2'
 a,b = get('/bypass',{'X-Lab-Bypass':'1'}),get('/bypass')
 check('bypass-can-populate-new-cached-response', {'bypassState':a['cache'],'nextState':b['cache'],'nextBody':b['body']},a['cache']=='BYPASS' and b['cache']=='HIT' and b['body']=='version:v2')
 state['version']='v3'
 a=get('/bypass',{'X-Lab-Nostore':'1'})
 b=get('/bypass',{'X-Lab-Bypass':'1','X-Lab-Nostore':'1'})
 c=get('/bypass')
 check('read-bypass-and-write-suppression-are-separate', {'noCacheOnlyState':a['cache'],'combinedState':b['cache'],'combinedBody':b['body'],'laterCachedBody':c['body']},a['cache']=='HIT' and b['cache']=='BYPASS' and b['body']=='version:v3' and c['body']=='version:v2' and c['cache']=='HIT')
 a=get('/revalidate');get('/stale');get('/strict');time.sleep(2.1)
 b=get('/revalidate');state['outage']=True
 stale,strict=get('/stale'),get('/strict')
 process.send_signal(signal.SIGQUIT);process.wait(timeout=10)
 logs=[json.loads(l) for l in (root/'access.log').read_text.splitlines if l.strip]
 rv=[l for l in logs if l['path']=='/revalidate'][-1]
 check('upstream-304-revalidates-body-for-client-200', {'clientStatus':b['status'],'upstreamStatus':rv['upstream'],'cacheState':b['cache'],'bodyRetained':a['body']==b['body'],'conditionalValidatorObserved':any(e['ifNoneMatch']=='"fixture-v1"' for e in events)}, b['status']==200 and rv['upstream']=='304' and b['cache']=='REVALIDATED' and b['body']==a['body'])
 sl=[l for l in logs if l['path']=='/stale'][-1]
 st=[l for l in logs if l['path']=='/strict'][-1]
 check('stale-policy-masks-origin-failure-in-client-status', {'staleClientStatus':stale['status'],'staleUpstreamStatus':sl['upstream'],'staleCacheState':stale['cache'],'strictClientStatus':strict['status'],'strictUpstreamStatus':st['upstream']},stale['status']==200 and sl['upstream']=='503' and stale['cache']=='STALE' and strict['status']==503 and st['upstream']=='503')
 result={'executedAt':datetime.now(timezone.utc).isoformat, 'nginxVersion':subprocess.run([binary,'-V'],capture_output=True,text=True,check=True).stderr.strip, 'pythonVersion':platform.python_version,'checks':checks,'passed':sum(c['passed'] for c in checks.values),'failed':sum(not c['passed'] for c in checks.values),'configuration':config,'originEvents':events,'responses':responses,'accessLog':logs,'scriptSha256':hashlib.sha256(pathlib.Path(__file__).read_bytes).hexdigest,'binarySha256':hashlib.sha256(pathlib.Path(binary).read_bytes).hexdigest,'scope':'Actual NGINX with one worker and synthetic Python origin on IPv4 loopback. GET only. No TLS, browser cache, authentication, CDN, production traffic, commercial purge API, load test or independent specialist review.'}
 finally:
 if process is not None and process.poll is None:
 process.send_signal(signal.SIGQUIT)
 try: process.wait(timeout=10)
 except subprocess.TimeoutExpired:
 process.kill;process.wait(timeout=5)
 output.close
 result['temporaryDirectoryRemoved']=not root.exists
 result['childExited']=process.returncode is not None
 return result
 finally:
 origin.shutdown;origin.server_close;thread.join(timeout=5)

if __name__ == '__main__':
 parser=argparse.ArgumentParser;parser.add_argument('--output',required=True);args=parser.parse_args
 binary=os.environ.get('DR_NGINX_BIN')
 if not binary or not pathlib.Path(binary).is_file:
 parser.error('DR_NGINX_BIN must identify an existing NGINX executable')
 result=run(str(pathlib.Path(binary).resolve))
 pathlib.Path(args.output).write_text(json.dumps(result,indent=2)+'\n')
 print(json.dumps({'passed':result['passed'],'failed':result['failed'],'temporaryDirectoryRemoved':result['temporaryDirectoryRemoved'],'childExited':result['childExited']}))
IN PRACTICE

In a fictional portal, /query?fund=A and /query?fund=B have different bodies. A key that loses fund can mix data without producing an HTTP error.

Common pitfalls

Confusing bypass with purge, removing private to raise hit ratio or assuming a custom header affects every cache on the path.

Related topics: The request and intended representation · Caching, variants, and validation · Diagnosis and time budgets

Take this idea with you

A correct cache must select the right body, respect the reuse contract and make read and storage decisions observable.

Create account

Reference: NGINX HTTP proxy module: cache controls · BigSavant HTTP/HTTPS 2026-09; HTTP RFCs 9110–9114; selected TLS 1.3 and NGINX/curl guidance