Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
ab6cf2c
feat: add ValkeyStorage with L1 fallback for cache() filter
larry-dalmeida May 21, 2026
535836a
feat: split valkey_fallback counter and add valkey_miss metric
larry-dalmeida May 26, 2026
0021d81
fix: address WP6 code review feedback
larry-dalmeida May 26, 2026
f062aa4
feat: add periodic lru_bytes gauge scrape every 10s
larry-dalmeida May 26, 2026
716bff8
feat: log storage errors with per-site messages
larry-dalmeida May 26, 2026
cac6a61
test: must-revalidate forces coalesce when stale
larry-dalmeida May 26, 2026
44e1d72
test: unsafe method + 4xx does not invalidate cached entry
larry-dalmeida May 26, 2026
e8f08ce
test: oversized LRU entry increments lru_oversized and is not stored
larry-dalmeida May 26, 2026
b64ab9d
test: reval_dropped and L1 fallback write verification
larry-dalmeida May 26, 2026
54739d1
feat: tag cache_status, cache_key, cache_ttl_remaining_ms on trace span
larry-dalmeida May 26, 2026
08e4865
cache: promote storage Set/Delete error logs from Debug to Warn
larry-dalmeida May 27, 2026
78503a7
Promote log to warn
larry-dalmeida May 28, 2026
eff5cbe
cache: injectable metrics, trace spans, lru_bytes scraper
larry-dalmeida May 28, 2026
d0d427c
Fix registration of cache filter
larry-dalmeida Jun 2, 2026
cd792c3
refactor: add l1TTL field to ValkeyStorage (write-through prep)
larry-dalmeida Jun 16, 2026
61f6d54
feat: write-through L1 warming on successful Valkey Set
larry-dalmeida Jun 16, 2026
1aa06ae
feat: L1-first reads with l1_hit counter in ValkeyStorage.Get
larry-dalmeida Jun 16, 2026
a42c47f
fix: update stale counter assertion after L1-first Get, add l1_hit to…
larry-dalmeida Jun 16, 2026
d1d7f8d
feat: wire --cache-l1-ttl flag through Options to ValkeyStorage
larry-dalmeida Jun 17, 2026
6e240e7
refactor: move --cache-l1-ttl config field and flag to Valkey section
larry-dalmeida Jun 17, 2026
e13b45a
style: use strings.SplitSeq in stripHopByHop and parseVaryNames
larry-dalmeida Jun 17, 2026
12daa1b
fix: add CacheL1TTL to defaultConfig and make Close() synchronous
larry-dalmeida Jun 18, 2026
4eb9e27
style: group cache config fields into dedicated //cache section in co…
larry-dalmeida Jun 18, 2026
ecbb64f
style: fix gofmt alignment of bgWg struct field comment
larry-dalmeida Jun 18, 2026
d1046bd
fix: address code review findings — vary sentinel invalidation, stub …
larry-dalmeida Jun 19, 2026
a0db0e1
docs: document Valkey L2 storage, write-through L1, and cache metrics
larry-dalmeida Jun 19, 2026
25900af
Address PR #4033 code review comments — move goroutines to spec level…
larry-dalmeida Jun 29, 2026
e96389e
fix: move SetWithExpire empty-result guard before error loop
larry-dalmeida Jun 29, 2026
eac532e
refactor: replace doRevalFn closure with direct filter reference in r…
larry-dalmeida Jun 29, 2026
0093f56
refactor: simplify Options struct comments per code review
larry-dalmeida Jul 1, 2026
9b6527e
fix: replace select-based Close guard with sync.Once to prevent race
larry-dalmeida Jul 1, 2026
7e13db3
feat: add reval_duration histogram metric to revalidation worker
larry-dalmeida Jul 1, 2026
986fa2e
feat: add QUERY method support to cache filter
larry-dalmeida Jul 1, 2026
d983c82
docs: clarify cacheFilter.Close no-op intent
larry-dalmeida Jul 2, 2026
6bc5d63
docs: fix forward reference in cacheFilter.Close comment
larry-dalmeida Jul 2, 2026
bb16ab5
feat: add filterCacheKey registry to reuse filter instances across ro…
larry-dalmeida Jul 3, 2026
c8d34a2
refactor: move filterCacheKey and cacheSpec before NewCacheFilter
larry-dalmeida Jul 3, 2026
df17860
feat: add DEL command to ValkeyRingClient
larry-dalmeida Jul 3, 2026
fbaa609
fix: add MeasureBackendZone stub to testMetrics
larry-dalmeida Jul 9, 2026
fcbeff0
docs: fix Cache section wording in operation.md
larry-dalmeida Jul 24, 2026
fa3cdda
docs: explain intentional double L1 lookup on Valkey Get error
larry-dalmeida Jul 24, 2026
1935a7f
docs: clarify thundering herd prevention is process-local in coalesce
larry-dalmeida Jul 24, 2026
6d7a157
fix: prevent send-on-closed panic in enqueueRevalidation and drain ca…
larry-dalmeida Jul 24, 2026
f6c4f54
fix: warm L1 on Valkey Get hit using remaining TTL
larry-dalmeida Jul 24, 2026
e0f0925
fix: add counter to track warming L1 on Valkey Get hit
larry-dalmeida Jul 24, 2026
f86c5ee
feat: add reval_queue_depth and reval_wait_duration metrics
larry-dalmeida Jul 24, 2026
7e41678
fix: handle QUERY body read error in enqueueRevalidation
larry-dalmeida Jul 24, 2026
cbaf8b6
fix: update docs/comments to clarify local only DELETE behavior
larry-dalmeida Jul 27, 2026
afe76a1
fix: return empty key on QUERY body read failure in cacheKey
larry-dalmeida Jul 28, 2026
956f8e0
fix: add error return to cacheSpec.Close to satisfy io.Closer
larry-dalmeida Aug 7, 2026
c7f3fd4
fix: wrap cacheSpec.Close in t.Cleanup and remove extra blank line in…
larry-dalmeida Aug 18, 2026
1afc705
fix: restore CacheL1TTL to Options struct and remove duplicate stale …
larry-dalmeida Aug 18, 2026
ce0e84a
refactor: remove cache_key span tag and key param from tagSpan
larry-dalmeida Aug 18, 2026
0b3a382
docs: fix cache key description and add QUERY method support in filte…
larry-dalmeida Aug 18, 2026
3c4818c
docs: expand cache operation docs — metrics, span tags, memory budget…
larry-dalmeida Aug 18, 2026
9046bc3
docs: add inline comments to cache Options fields
larry-dalmeida Aug 18, 2026
76c947d
refactor: replace recover() with context-based shutdown in cache filter
larry-dalmeida Aug 19, 2026
34d00d1
style: gofmt alignment fix in cacheFilter struct after context refactor
larry-dalmeida Aug 19, 2026
c8ac0bb
refactor: remove QUERY method support — to be reintroduced in feat/ca…
larry-dalmeida Aug 19, 2026
0d9b8a1
refactor: add cache. prefix to all metrics and route hit/miss/stale/c…
larry-dalmeida Aug 19, 2026
1b7050f
docs: replace SIE acronym with stale-if-error in comments
larry-dalmeida Aug 19, 2026
96602e5
docs: clarify shared L1 cache key semantics and per-user key isolation
larry-dalmeida Aug 19, 2026
5398e72
docs: clarify cache invalidation — no out-of-band API, document avail…
larry-dalmeida Aug 19, 2026
bd7825f
docs: clarify process restart only clears L1; Valkey data persists
larry-dalmeida Aug 19, 2026
473696a
docs: clarify coalescing is per-route and document RFC 9111 §3.5 Auth…
larry-dalmeida Aug 19, 2026
61fb35a
refactor: rename metricsScraper to updateMetrics
larry-dalmeida Aug 25, 2026
761e2dc
refactor: remove cacheFilter.Close no-op, io.Closer assertio
larry-dalmeida Aug 25, 2026
2d0defc
fix: treat valkey Get errors as misses instead of falling back to L1
larry-dalmeida Aug 25, 2026
8f53bc0
Merge branch 'master' into feat/cache-client-side-valkey-hash-ring
larry-dalmeida Aug 25, 2026
ea96966
fix: update TestCacheFilter_LRUBytesGaugeUpdatesWithoutEviction to pa…
larry-dalmeida Aug 25, 2026
6ae452a
fix: update docs and metrics to reflect valkey Get error handling cha…
larry-dalmeida Aug 27, 2026
e99ffd8
fix: rename l1_warm_from_valkey → l2_hit
larry-dalmeida Aug 27, 2026
140f65a
fix: formatting
larry-dalmeida Aug 27, 2026
fbde416
fix: don't serve stale L1 entries when Valkey may have fresher copy
larry-dalmeida Aug 28, 2026
de56cb8
refactor: use isCacheableMethod on fresh HIT conditional path
larry-dalmeida Aug 28, 2026
44b321f
feat: add cache.storage_error counter at all storage error sites
larry-dalmeida Aug 28, 2026
d919e38
docs: document cache.storage_error counter in operation guide
larry-dalmeida Aug 28, 2026
b718007
docs: clarify --swarm-valkey-urls wires both ratelimit and cache
larry-dalmeida Aug 28, 2026
923ddc8
add --enable-l2-cache flag to opt in to Valkey as cache L2 backing store
larry-dalmeida Sep 1, 2026
d5b283d
rename valkey_miss/get_error/set_fallback metrics to l2_ prefix for c…
larry-dalmeida Sep 1, 2026
3024f17
refactor: cache filter should not know about the implemtnation of L2 …
szuecs Sep 1, 2026
ee25732
build: vet was not part of lint target
szuecs Sep 1, 2026
ebd6069
refactor: default nil enable sets it
szuecs Sep 1, 2026
91f3bee
refactor: set cache client via options and default to valkey ring cli…
szuecs Sep 1, 2026
ee98d3b
fix: test cases should use NewL2.. function
szuecs Sep 1, 2026
18ffb50
test: add more test coverage and integration tests with proxytest tes…
szuecs Sep 1, 2026
5e7f074
ai: adding how to use mockMetrics
szuecs Sep 1, 2026
d071612
refactor: rewrite empty interface to any
szuecs Sep 1, 2026
9f7f6ce
add l2_storage coverage
szuecs Sep 1, 2026
840cf86
doc: use PR description to add storage architecture doc
szuecs Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .agents/skills/tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,20 @@ func TestSetRequestHeader(t *testing.T) {
}
}
```

If you need metrics.Metrics implementation to inspect metrics, you can use:

```go
mockMetrics := &metricstest.MockMetrics{}
// some code ..

// inspect counters
mockMetrics.WithCounters(func(counters map[string]int64) {
if n := counters["a-counter"]; n == int64(5) { t.Fatalf("Failed to get expected counter value 5, got: %d", n) }
})

// inspect Gauges
mockMetrics.WithGauges(func(g map[string]float64) {
...
})
```
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ fuzz: ## run all fuzz tests
$(MAKE) -C fuzz $(MAKECMDGOALS)

.PHONY: lint
lint: build staticcheck ## run all linters
lint: build vet staticcheck ## run all linters

.PHONY: clean
clean: ## clean temporary files and directories
Expand Down
15 changes: 15 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,11 @@ type Config struct {
SwarmStaticSelf string `yaml:"swarm-static-self"`
SwarmStaticOther string `yaml:"swarm-static-other"`

// cache
CacheL1TTL time.Duration `yaml:"cache-l1-ttl"`
CacheL1MaxMemoryBytes int64 `yaml:"cache-l1-max-memory-bytes"`
EnableL2Cache bool `yaml:"enable-l2-cache"`

ClusterRatelimitMaxGroupShards int `yaml:"cluster-ratelimit-max-group-shards"`

EnableLua bool `yaml:"enable-lua"`
Expand Down Expand Up @@ -745,6 +750,11 @@ func NewConfig() *Config {
flag.StringVar(&cfg.SwarmStaticSelf, "swarm-static-self", "", "set static swarm self node, for example 127.0.0.1:9001")
flag.StringVar(&cfg.SwarmStaticOther, "swarm-static-other", "", "set static swarm all nodes, for example 127.0.0.1:9002,127.0.0.1:9003")

// cache
flag.DurationVar(&cfg.CacheL1TTL, "cache-l1-ttl", 60*time.Second, "maximum TTL for write-through L1 warming in the cache() filter when Valkey is configured; set to 0 to disable (write-around)")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't be there a --cache-l2-ttl also?

@larry-dalmeida larry-dalmeida Jul 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Valkey TTL is derived directly from Cache-Control headers: entry.TTL + max(StaleIfError, StaleWhileRevalidate).

There is no separate operator cap because Valkey is the shared cache store and its TTL is the authoritative freshness signal - shortening it would cause unnecessary upstream traffic for clients that would otherwise be served a fresh response. --cache-l1-ttl exists for a different reason: L1 is a local in-process optimisation layer, and bounding its TTL prevents a single long-lived response from occupying local memory on every process indefinitely.

Note that force mode still holds fully - and it already gives operators exactly what --cache-l2-ttl would provide, just at the route level rather than the process level.

A --cache-l2-ttl flag would make sense if there's a concrete operator requirement to cap Valkey retention independently of upstream headers.

I've left it out for now to keep the behaviour strictly header-driven, but happy to add it if you have a specific use case in mind.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@a4180p can we resolve this thread?

flag.Int64Var(&cfg.CacheL1MaxMemoryBytes, "cache-l1-max-memory-bytes", 0, "maximum memory budget in bytes for the cache() filter's in-process LRU (L1); defaults to 25% of cgroup memory limit or 2 GB if unreadable")
flag.BoolVar(&cfg.EnableL2Cache, "enable-l2-cache", false, "enable Valkey as L2 backing store for the cache() filter when --swarm-valkey-urls is configured; by default only in-process LRU (L1) is used")

flag.IntVar(&cfg.ClusterRatelimitMaxGroupShards, "cluster-ratelimit-max-group-shards", 1, "sets the maximum number of group shards for the clusterRatelimit filter")

flag.BoolVar(&cfg.EnableLua, "enable-lua", false, "enable the Lua scripting engine to be able to use the lua() filter")
Expand Down Expand Up @@ -1210,6 +1220,11 @@ func (c *Config) ToOptions() skipper.Options {
SwarmStaticSelf: c.SwarmStaticSelf,
SwarmStaticOther: c.SwarmStaticOther,

// cache
CacheL1TTL: c.CacheL1TTL,
ResponseCacheMaxMemoryBytes: c.CacheL1MaxMemoryBytes,
EnableL2Cache: c.EnableL2Cache,

ClusterRatelimitMaxGroupShards: c.ClusterRatelimitMaxGroupShards,

EnableLua: c.EnableLua,
Expand Down
1 change: 1 addition & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ func defaultConfig(with func(*Config)) *Config {
SwarmValkeyDialTimeout: 25 * time.Millisecond,
SwarmValkeyKeepAlive: time.Second,
SwarmValkeyUpdateInterval: 10 * time.Second,
CacheL1TTL: 60 * time.Second,
SwarmKubernetesNamespace: "kube-system",
SwarmKubernetesLabelSelectorKey: "application",
SwarmKubernetesLabelSelectorValue: "skipper-ingress",
Expand Down
107 changes: 107 additions & 0 deletions docs/operation/operation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1960,3 +1960,110 @@ will be changed to
```
r: SourceFromLast("9.0.0.0/24","2001:67c:20a0::/48") -> ...`
```

## Cache

By default entries are stored in an in-process LRU (L1) local to each Skipper process.
When `--swarm-valkey-urls` is configured and `--enable-l2-cache` is set, Valkey serves as a
shared backing store (L2) accessible by all Skipper instances via a client-side consistent
hash ring; every read checks L1 first and an L1 hit returns without contacting L2 cache.
Without `--enable-l2-cache`, `--swarm-valkey-urls` wires Valkey into ratelimit only and the
`cache()` filter uses L1 exclusively.

On every successful L2 write the entry is also written to L1
(write-through) with a TTL of `min(--cache-l1-ttl, entry.TTL)`. On a L2
read hit, L1 is warmed with `min(--cache-l1-ttl, remaining freshness)` —
remaining freshness (`entry.TTL - age`) is used rather than the original TTL to
prevent L1 from serving the entry beyond L2's actual expiry. The default is
60 seconds, bounding how long Skipper serves a locally-cached entry before
re-consulting L2. Set `--cache-l1-ttl=0` to disable L1 warming and
restore write-around behaviour. Note: on L2 write errors (`l2_set_fallback`), L1
is always used as a fallback regardless of `--cache-l1-ttl`; L2 read errors
(`l2_get_error`) are treated as cache misses.

When an upstream responds successfully to an unsafe method (`POST`, `PUT`, `DELETE`, `PATCH`),
the filter removes the cached entry for that URL from both L2 and the local L1.
Only the local process's L1 is cleared — other Skipper processes in the fleet retain their
own L1 copies until each entry's warmed TTL (bounded by `--cache-l1-ttl`) expires naturally.
Set `--cache-l1-ttl` accordingly to bound the stale window after an invalidation.

There is no out-of-band operator invalidation API. To clear the cache outside the normal
unsafe-method path, options are: wait for TTL expiry, restart the Skipper process (clears L1 in-memory cache only; L2 data persists), or
delete the key directly in L2.

Concurrent cold-miss requests for the same route and key within one Skipper process are
coalesced into a single upstream fetch (thundering-herd protection). Requests arriving via
different routes are not coalesced even if they target the same upstream URL, because the
route ID is part of the cache key. This protection is also process-local: a fleet of N
instances may still issue up to N simultaneous origin requests on a cold miss.

The L1 memory budget defaults to 25% of the container's cgroup memory limit,
falling back to 2 GB if the limit is unreadable. Override with
`--cache-l1-max-memory-bytes` (or programmatically via `skipper.Options.ResponseCacheMaxMemoryBytes`).

!!! note
An in-process LRU (L1) is shared across all `cache()` filter instances in the same process.
Two routes or two users that produce the same cache key will share the cached entry — the
second request is served whatever the first stored. Ensure the cache key includes all
dimensions that distinguish responses (e.g. add `Authorization` to `keyHeaders` for
per-user routes). The L1 storage budget is divided evenly across 256 internal shards; a
single entry larger than one shard's budget is dropped with a warning log and increments `cache.lru_oversized`.

Comment thread
szuecs marked this conversation as resolved.
### Storage architecture

- L1 implementation is in-memory (25% of memory set by cgroup or 2GB fix size)
- L2 implementation is for example skpnet.ValkeyRingClient, reusing Valkey ring shards if available.

```mermaid
flowchart TD
A[Incoming Request] --> B["Skipper cache() filter"]
B --> C{L1 LRUStorage}
C -->|cache.l1_hit — fresh hit| S[Entry Served]
C -->|cache.l2_miss| D{L2Storage}
C -->|cache.stale hit — no l1_hit| D
D -->|cache.l2_hit — key found| S
D -->|cache.l2_miss — key absent| E[Origin Fetch\n CDN]
D -->|cache.l2_get_error — error / timeout| E
E -->|write back: Set warms L2 + L1<br/>min TTL: cache-l1-ttl vs entry.TTL| S
```

**Write path:** successful L2 Set warms L1 with min(--cache-l1-ttl, entry.TTL) (default 60s). L2 Get hit warms L1 with min(--cache-l1-ttl, remaining freshness). L1 is also populated on L2 Set errors (fallback). Set --cache-l1-ttl=0 for write-around.

**Read path:** L1 checked first. Fresh L1 hit → returns immediately (increments `cache.l1_hit`), no L2 call. Stale L1 hit (past TTL but within the stale retention window) → falls through to L2 without incrementing `cache.l1_hit`. L1 miss → L2. L2 hit → increments `cache.l2_hit`, warms L1 with `min(--cache-l1-ttl, remaining freshness)` when `--cache-l1-ttl > 0`, returns entry. L2 miss (nil) → cold miss (increments `cache.l2_miss`). L2 error → increments `cache.l2_get_error`, treated as a cold miss.

### Metrics
Comment thread
szuecs marked this conversation as resolved.

**Cache outcomes (always active):**

- `cache.hit`: Counter, request served from cache without contacting the upstream
- `cache.miss`: Counter, request not in cache; upstream was contacted
- `cache.stale`: Counter, stale entry served while background revalidation was enqueued
- `cache.coalesce_error`: Counter, singleflight cold-miss fetch returned an error

**LRU (always active):**

- `cache.lru_eviction`: Counter, incremented each time an L1 entry is evicted due to memory pressure
- `cache.lru_bytes`: Gauge, current L1 usage in bytes (sampled every 10s)
- `cache.lru_oversized`: Counter, incremented when an entry is too large for any shard and dropped

**Revalidation (always active):**

- `cache.reval_queue_depth`: Gauge, current number of pending revalidation jobs in the queue (sampled every 10s)
- `cache.reval_wait_duration`: Histogram, time a revalidation job spent waiting in the queue before the worker picked it up
- `cache.reval_dropped`: Counter, revalidation jobs dropped because the queue was full or body read failed
- `cache.reval_error`: Counter, background revalidation fetch failures
- `cache.reval_duration`: Histogram, end-to-end duration of each background revalidation job

**L2 (for example if Valkey is configured):**

- `cache.l1_hit`: Counter, L1 hits that bypassed L2
- `cache.l2_miss`: Counter, L2 misses that proceeded to an upstream fetch
- `cache.l2_get_error`: Counter, L2 Get errors — request treated as a cache miss, fetched from origin
- `cache.l2_set_fallback`: Counter, L2 Set errors — entry written to L1 only (not L2)
- `cache.l2_hit`: Counter, successful L2 Get (entry returned from L2); L1 is warmed as a side-effect when `--cache-l1-ttl > 0`
- `cache.storage_error`: Counter, any storage operation (Set or Delete) failed — covers both L2 failures and L1 eviction-path errors; the request is still served correctly

**OpenTracing span tags (set on every request when a span is active):**

- `cache_status`: `"hit"`, `"miss"`, or `"stale"`
- `cache_ttl_remaining_ms`: remaining freshness in milliseconds (only set on hits)
20 changes: 6 additions & 14 deletions docs/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -4048,7 +4048,7 @@ Force mode with per-tenant cache key isolation:

**Cache key**

The key is derived from route ID + HTTP method + host + path + query string,
The key is derived from route ID + scheme + host + path + query string,
hashed with SHA-256 for uniform shard distribution.
Route ID is included so entries from different routes never collide when sharing
the same storage instance. Additional request headers can be folded in via
Expand All @@ -4067,22 +4067,14 @@ matching the same key. It has no awareness of other filters in the chain.
* **`Authorization` is not in the key by default.** Responses are stored and
served without regard to caller identity. To isolate per-user responses, add
`Authorization` to `keyHeaders`; without it, a response stored for one user
will be served to all others on the same path.
will be served to all others on the same path. Note: in RFC mode, if the
request carries an `Authorization` header and the upstream does not respond
with `Cache-Control: public` or `must-revalidate`, the response is silently
not stored (RFC 9111 §3.5). Use force mode or ensure the upstream sets
`Cache-Control: public` if you want authenticated responses cached.
* **`Cache-Control: private` is ignored in force mode.** Audit the upstream
response before enabling force mode on any authenticated route.

!!! note
The LRU store is shared across all `cache()` filter instances. The storage
budget is divided evenly across 256 internal shards; a single entry larger
than one shard's budget is dropped with a warning log.

!!! note
Three metrics track LRU behaviour: `lru_eviction` (counter, incremented each
time an entry is evicted due to memory pressure), `lru_bytes` (gauge,
updated on every eviction to reflect current storage usage in bytes), and
`lru_oversized` (counter, incremented when an entry is too large to fit in
any shard and is silently dropped).

!!! note
`s-maxage` implies `proxy-revalidate` per [RFC 9111 §5.2.2.10](https://www.rfc-editor.org/rfc/rfc9111#section-5.2.2.10): stale entries
stored under `s-maxage` are never served without revalidation, regardless of
Expand Down
Loading
Loading