Aperio can answer repeated GET requests from an in-memory cache on the
server, instead of forwarding every one down the tunnel to your client and
backend. On a hot, cacheable URL this removes the tunnel round-trip entirely,
lower latency for visitors and far less load on the backend.
Caching is off by default and strictly opt-in on both sides.
A response is only cached when three independent conditions all agree:
- The server operator enabled the cache,
cache: trueinaperio-server.yaml(envAPERIO_CACHE=1; the dashboard's live settings persist it as acache_enabledoverride). This provisions the shared in-memory cache and its memory budget. - The service owner opted the service in,
cache: truein the client config (perservices:entry, or top-level, orAPERIO_CACHE=1on the client). Only the service owner knows whether their responses are safe to cache, so this consent lives with the client and is announced over the tunnel. - The response says it is cacheable, the cache is strictly
Cache-Control-driven (see below).
This separation is deliberate: the operator provides the capability, the service owner declares eligibility, and the response itself has the final say. None of the three can cache on its own.
If a service sets
cache: truebut the server cache is off, the opt-in silently does nothing. Aperio surfaces this so it is not a mystery: the server logs a one-time warning per client (… requested response caching … but the server cache is disabled (APERIO_CACHE off); the opt-in is ignored), and the dashboard's Clients table shows acache offbadge on that connection. Fix it by enabling the server cache, or drop the flag from the service.
Only responses that explicitly allow shared caching, for exactly the lifetime they advertise:
- A cacheable
Cache-Control(max-age/s-maxage) and none ofno-store,no-cache,private. - No
Varyand noSet-Cookie(responses that depend on the request or carry per-user state are never stored). - Only credential-less plain
GETs are answered from the cache (a request carryingAuthorization/cookies bypasses it).
Nothing is cached implicitly, if your backend never sends Cache-Control,
nothing is stored, no matter the flags.
Negative caching is the one exception, and it is off by default.
cache_negative_ttl (env APERIO_CACHE_NEGATIVE_TTL) holds 404 and 410
answers for a few seconds without asking the backend's permission, because a
hot missing URL, a scanner, a broken link on a busy page, otherwise reaches
the backend on every request to be told nothing is there each time. Keep it
short: it is the one setting here that can serve a "not found" for a resource
that has since appeared. A response carrying Vary, Set-Cookie or a
no-store/no-cache/private Cache-Control is still never stored.
- Hits carry
x-aperio-cache: hitand anAgeheader. - Edge
304: entries without a backend validator get a synthesizedETag; a matchingIf-None-Matchis answered304 Not Modifiedat the edge with no tunnel round-trip. - Single-flight: concurrent identical cacheable
GETs collapse into one upstream fetch, followers wait for the leader and answer from the freshly stored entry, so expiry on a hot URL cannot stampede the backend. stale-while-revalidate(RFC 5861): a response advertisingstale-while-revalidate=Nkeeps serving forNseconds past expiry (markedx-aperio-stale) while one background revalidation refreshes it, visitors never wait on the refresh.Rangerequests: single-range requests (video scrubbing, resumable downloads) are sliced from the stored full body at the edge,206 Partial ContentwithAccept-Ranges/Content-Range,416when out of range, honoringIf-Range, without re-traversing the tunnel. Ranges are answered here only while a cached entry covers the URL; otherwise they reach the backend, or the client's own file server in static-file mode, which answers them too but serves the full200for anIf-Rangerequest rather than comparing validators it never issued.- Purge:
POST /aperio/api/cache/purge(admin) drops entries byhostnameand/orpath_prefix(empty body = the whole cache) for immediate invalidation after a deploy.
resilience: true on a service (needs cache: true and the server cache) lets
cached responses keep answering visitors while no healthy client is
connected, instead of failing with 504. Fresh-or-expired entries answer up
to the cache_max_stale (env APERIO_CACHE_MAX_STALE) window past their lifetime, marked
x-aperio-stale: true once past it and always with an Age header. The moment
a client reconnects, normal proxying takes over. See
Client Resilience.
This holds under the closed posture too. With default_access: deny a route
whose client has gone is normally refused before anything consults the cache,
which is the one condition serve-stale exists for; a resilient entry is
answered with instead. It cannot disclose a route the posture hides, because
only a client that asked for serve-stale leaves an entry behind, and a route
with no resilient entry still gets the refusal.
Every setting is shown by its yaml key (env var in parentheses). Server keys go
in aperio-server.yaml, client keys in aperio.yaml (per services: entry).
| yaml key | Where | Effect | Default |
|---|---|---|---|
cache (env APERIO_CACHE) |
server | Enable the shared response cache. | 0 |
cache (env APERIO_CACHE) |
client, per service | Opt this service in. | 0 |
cache_max_bytes (env APERIO_CACHE_MAX_BYTES) |
server | Total in-memory budget; inserting past it evicts the entries closest to expiry, and a body larger than a quarter of the budget is never cached. | 67108864 (64 MB) |
resilience (env APERIO_RESILIENCE) |
client, per service | Serve stale while no client is connected. | 0 |
cache_max_stale (env APERIO_CACHE_MAX_STALE) |
server | Serve-stale window in seconds; 0 disables it. |
3600 |
cache_negative_ttl (env APERIO_CACHE_NEGATIVE_TTL) |
server | Seconds to hold a 404/410 so a hot missing URL cannot hammer the backend; 0 disables it. |
0 |
The full option reference lives in Configuration; the end-to-end request path is in Tunnel Protocol & Advanced Features, and the throughput trade-offs in Performance Tuning.
Copy-and-adapt config pairs for this topic:
cache: server-side GET cache, opted in per serviceresilience: serve stale while offline, per service