16 KiB
HTTP cache — HOT, COLD and invalidation
RustProxy provides an optional shared HTTP response cache with a bounded in-memory HOT tier and optional persistent-disk COLD tier. Both the global cache and a Vhost's effective policy must permit caching. COLD storage is disabled by default, and newly created Vhosts do not automatically cache dynamic content.
Enable caching safely
- Open Settings → Cache, enable the global cache and choose memory limits. Enable COLD only if you want durable disk storage and have enough private disk capacity.
- On the appropriate Vhost, enable cache for public, repeatable responses. Restrict or disable it for login, personalized pages, uploads, APIs and responses carrying secrets.
- Ensure the origin supplies a valid public freshness policy, or configure a safe default TTL where supported.
Cache-Control: privateandno-storemust prevent shared storage. - Optionally enable HIT-only diagnostics. Send repeated identical requests using
curl, then inspect Cache → Domains / Stored objects forMISS → HIT. - Review Varnish-compatible cache policy, enabled by default, then check TTL, revalidation and invalidation after updating the origin's content or configuration.
Varnish-compatible cache policy (0.34.7)
Settings → Cache → Varnish-compatible cache policy (cache.varnish_compatible, enabled by default) avoids needless cache replacement on browser hard reloads. On a fresh object it ignores only the client's Cache-Control: no-cache, max-age=0 and Pragma: no-cache for cache lookup/freshness. It does not remove or modify these request headers when fetching from the origin, and it does not alter cache identity or request conditionals.
Set Default grace (seconds) in Settings → Cache (default 60, range 0..604800). This affects only eligible objects in compatibility mode and never overwrites explicit origin stale-while-revalidate, including =0. Use the adjacent i information icon for the short operator explanation (PL/EN). Disabling the option restores the original strict client reload semantics. Changing modes invalidates currently stored representations across HOT/COLD so they are refilled under one consistent policy; no process restart is needed. Existing JSON configurations with no field inherit true. The setting also applies to eligible resources of the embedded Web Server.
| Condition | Enabled: Varnish-style optimized, RustProxy protections retained | Disabled: legacy strict |
|---|---|---|
Client no-cache, max-age=0, Pragma: no-cache |
Fresh HIT without replacing object or resetting Age / HIT counter |
Can force origin revalidation (and 200 replacement without an origin validator) |
Client no-store |
BYPASS: do not use or populate a shared representation | Same |
Client only-if-cached |
Fresh/stale eligible HIT, or 504 if no reusable representation; never fetch origin on MISS | Same |
Client min-fresh, max-stale, positive max-age |
Keep constraints; an otherwise fresh object can be rejected by min-fresh, while eligible stale entries can be served by max-stale |
Same |
Origin no-cache with ETag/Last-Modified, fallback TTL = 0 |
Store for conditional revalidation (304) but never fresh-HIT without checking origin |
Same |
Origin public, no-cache and explicit fallback TTL > 0 |
Treat the administrator's configured Fallback TTL for public responses as shared freshness (capped by max TTL), even when the origin has no validator; this explicitly overrides origin no-cache |
Validator-only revalidation, no override |
Origin max-age=0 / already expired, with stale-if-error, a validator or client max-stale |
Retain the expired representation; it does not become a fresh HIT. Can serve stale on an eligible error, revalidate, or honor explicit client allowance | Same |
Origin Surrogate-Control: no-store |
Do not store, even when a fallback TTL is configured; this is an additional compatibility-mode safeguard | Legacy response policy |
Origin private, no-store, Set-Cookie, invalid Vary |
Must not enter shared cache; configured static-cookie exceptions do not override response privacy | Same |
| Eligible cached object after TTL | Implicit background-refresh grace up to 60 seconds by default (configurable with cache.varnish_grace_seconds, 0 disables it) only for entries initially fresh with no origin stale-while-revalidate directive; explicit stale-while-revalidate=0 disables implicit grace. Existing must-revalidate / proxy-revalidate restrictions still apply |
No implicit grace |
Cookies, Authorization, Vary, per-vhost exclusions/force, query/range restrictions, BAN/PURGE, request collapse, transforms, compression, HOT/COLD |
Existing RustProxy mechanisms remain in effect | Same |
Important: using a positive fallback TTL to reuse an origin response marked public, no-cache is an explicit administrative freshness override. It can serve outdated content for that duration; leave fallback TTL at 0 if the backend's no-cache must always be honored. The fallback override applies only to responses carrying public, not to private/no-store/Set-Cookie responses or Surrogate-Control: no-store. The configured max_ttl_seconds still caps remaining freshness. Without fallback TTL, origin no-cache still requires an origin validator to be stored. An origin max-age=0 is never made fresh merely because stale-if-error is present.
This is a Varnish-inspired policy subset, not full Varnish binary/VCL compatibility. RustProxy deliberately keeps an explicit zero-by-default fallback TTL instead of Varnish's implicit 120-second default, retains client controls beyond hard reload, enforces multi-tenant and cookie/Authorization safety rules, and does not implement hit-for-miss/hit-for-pass markers. The current compatibility-specific Surrogate-Control safeguard handles no-store; it does not interpret every Surrogate-Control extension or use Surrogate-Control max-age as a TTL override. Different VCL configurations can behave differently.
To verify with real GET requests (not curl -I -X GET):
URL=https://example.com/ui/style.css
curl -sS --compressed -D - -o /dev/null "$URL" | grep -iE '^(HTTP/|age:|x-cache:|x-cache-hits:|x-cache-reason:)'
curl -sS --compressed -H 'Cache-Control: no-cache' -H 'Pragma: no-cache' -D - -o /dev/null "$URL" | grep -iE '^(HTTP/|age:|x-cache:|x-cache-hits:|x-cache-reason:)'
curl -sS --compressed -D - -o /dev/null "$URL" | grep -iE '^(HTTP/|age:|x-cache:|x-cache-hits:|x-cache-reason:)'
With the policy enabled, the middle request stays HIT if the entry is fresh. Check safeguards separately: an uncached URL with Cache-Control: only-if-cached must return 504 without reaching the backend; a stale only-if-cached HIT must not start background validation/proactive refresh. min-fresh and max-stale still constrain stale reuse: an explicit max-stale=0 must not be bypassed by the configured implicit grace. Cache-Control: no-store must bypass an existing fresh entry.
Vhost editor options
In Vhosts → Cache, overrides for static extensions, bypass path prefixes and unsafe force targets each place the enable/override checkbox immediately above the associated multiline list. The fields are disabled while their individual override is off; their saved values and cache policy are unchanged. Never enable unsafe forced caching for personalized responses.
Stored object rules
- Reuse is scoped by frontend scheme, Host, exact client URL, Vhost/content policy and eligible
Varydimensions. HTTP and HTTPS objects do not cross-share. - GET and suitable HEAD requests may reuse a full object; changing request methods do not silently invalidate all cached GET results.
- Authorization, dynamic cookies,
Set-Cookie, sticky-affinity cookies, Tinyauth and other private signals normally cause BYPASS. A limited static-file cookie exception exists for configured static extensions and does not overrideprivate,no-store,Set-CookieorVary: Cookie. - Origin status, response size, TTL, unsupported metadata, range/conditional semantics and request/client cache directives can prevent fill or require validation.
- A Web Service selected as a Vhost source uses the same cache policy for eligible local files. File changes can be checked with
ETag/Last-Modified; a 304 validation may refresh a stored body without downloading it again.
Freshness and diagnostics
| State | Interpretation |
|---|---|
MISS |
No suitable fresh cached representation was found; the origin was consulted. The response may still be ineligible for storage. |
HIT |
A matching stored object was reused. |
BYPASS |
Cache was disabled or a policy intentionally skipped it (for example authorization, cookies or a path exclusion). |
| Revalidated | An existing representation was checked against the origin; a 304 can retain the body. |
Age is total HTTP age, not only the time spent in RustProxy. The optional X-Cache-Resident-Age reports the cache residency interval. Debug headers such as X-Cache, X-Cache-Hits and reason labels are optional and should be enabled deliberately. With varnish_compatible=true, browser hard reloads do not force revalidation while an object remains fresh. In strict mode hard reloads can force origin revalidation; compare with curl when investigating.
Example, replacing the listener and Host values with yours:
for n in 1 2 3; do
curl -sS -D - -o /dev/null -H 'Host: app.example.com' \
http://127.0.0.1:8080/public.css
sleep 2
done
Look for cache reason labels including request_cookie, request_authorization, response_cache_control, route_disabled and cache_hit when diagnostic headers are enabled. A MISS alone does not prove the object was admitted to HOT/COLD.
Cache response headers and upstream diagnostics (0.34.12)
RFC 9211: Cache-Status is a structured list of cache hops, ordered from the origin-side cache toward the client. RustProxy does not replace or suppress an upstream Cache-Status. When Settings → Cache → Expose Cache-Status is enabled and RustProxy makes a cache decision, it appends its own RustProxy member to the existing list; multiple HTTP header fields are an equivalent wire representation of the same list. There is no backend=hit/miss parameter: the upstream's own members already describe its cache decisions.
For example, with an upstream HIT and a RustProxy HIT:
Cache-Status: BackendCache; hit; ttl=500
Cache-Status: RustProxy; hit; ttl=482; detail="cache_hit"
X-Cache: HIT
On first fill, RustProxy uses RustProxy; fwd=miss; detail="cache_miss" (unless another forwarding reason applies). A cache policy BYPASS uses fwd=bypass, and reuse of a stored body following origin 304 validation is reported as fwd=stale; fwd-status=304, not a fresh unforwarded RFC 9211 HIT. The existing X-Cache: HIT for that validation remains for legacy monitoring compatibility. The ttl parameter counts remaining seconds for a RustProxy HIT, not the upstream's TTL, and detail is a fixed RustProxy reason code. The optional transformed extension on RustProxy's member is applied only to its own entry.
Legacy collisions: X-Cache, X-Cache-Hits, X-Cache-Reason and X-Cache-Resident-Age are non-standard response fields. They do not define interoperable append/list semantics. When RustProxy is configured to emit a particular field, it owns that field and copies all origin values (if any) into X-Upstream-Cache, X-Upstream-Cache-Hits, X-Upstream-Cache-Reason, or X-Upstream-Cache-Resident-Age respectively. For example:
X-Cache: HIT
X-Upstream-Cache: MISS
Cache-Status: BackendCache; fwd=miss
Cache-Status: RustProxy; hit; ttl=482; detail="cache_hit"
Here, the upstream MISS is the original backend state when RustProxy filled the object; it is not a fresh backend measurement on every RustProxy HIT. X-Cache-Status is forwarded unchanged because RustProxy does not generate that field. The X-Upstream-* aliases are a compatibility convenience, not RFC 9211; for new integrations use Cache-Status instead. Upstream headers are relayed as provided, without verifying the upstream's diagnostic accuracy.
Display switches: Expose cache status, Expose cache hits, Expose cache resident age, Expose Cache-Status and Only on HIT control only RustProxy-generated headers, not origin-supplied headers. Even when RustProxy caching is disabled, or a MISS/BYPASS is hidden by the Only on HIT option, backend-provided cache diagnostics remain visible. The Via header is governed separately by its configured cache policy. Backend diagnostics persist with eligible cached objects in HOT and COLD; responses which were never cacheable are still forwarded with their upstream fields intact.
To verify on a cacheable URL, use GET twice, without forced reload:
URL=https://example.com/public.css
for n in 1 2; do
curl -sS -D - -o /dev/null "$URL" | grep -iE '^(HTTP/|age:|cache-status:|x-cache:|x-upstream-cache:|x-cache-hits:|x-upstream-cache-hits:)'
done
The initial request can show RustProxy; fwd=miss; an eligible repeat should show RustProxy; hit; ttl=.... If the origin sends no Cache-Status, there is no upstream member to display. If RustProxy diagnostic output is off, any visible Cache-Status is from upstream; absence of a RustProxy member is expected. The actual cache outcome should also be checked in server request logs.
Advanced behavior
- Budget limits: HOT RAM capacity, object count/size, transient fill memory and optional COLD byte/object limits. Configure based on real workload and monitor disk/RSS.
- HOT/COLD movement: entries may be demoted, recovered or expired; a periodic/explicit COLD cleanup is available. Cache storage is not a permanent archive.
- Request collapsing: eligible concurrent MISS requests can share a bounded fill, limiting origin bursts.
- Stale-if-error and refresh: conditional options can help on eligible backend failures, but are not an unconditional stale-serving guarantee. Web Server local-file validation has a distinct policy.
- Compression/transformation: cached variants and validators must remain compatible with client encoding and content-modification settings.
Clear or invalidate content
Authenticated Cache page/API operations can purge selected entries, domains or larger scopes. The optional public HTTP PURGE/BAN interface is disabled by default. Enable it only by listing trusted client IPs/CIDRs under Settings → Cache → HTTP BAN / PURGE IP allowlist; operations are additionally subject to Vhost binding and normal access controls. PURGE addresses an exact URL; BAN applies the supported bounded matching rules. Never allow untrusted Internet clients to purge all tenants' objects.
In 0.34.0, changing operational Vhost settings is intended to preserve compatible HOT/COLD objects; content-affecting changes invalidate affected scopes and COLD carries an additional fingerprint against stale resurrection. This new behavior still needs release-level verification, so validate cache after configuration edits and during upgrades. Earlier persistent cache entries may need refilling following format/compatibility transitions.
Troubleshooting
- Always BYPASS: check both cache enable switches, login/cookies, exclusion rules, methods, Tinyauth and sticky sessions.
- MISS never becomes HIT: check response
Cache-Control, status, object size,Vary,Set-Cookie, incomplete origin streams and whether the same scheme/Host/query is used. - Content unexpectedly old: inspect Age, TTL, validators, stale rules and previous invalidation; purge only affected content after confirming its identity.
- Memory/disk pressure: review HOT/COLD budgets and cleanup, rather than increasing limits without measurements.
See Monitoring for cache telemetry and Security for privacy boundaries.