FRbext.dev
DocsServer-Side Includes
On this page

Caching includes

Includes get their own cache lifecycle independent of the page. Declare a TTL on the element, and bext stores the resolved HTML in its tiered cache; subsequent requests serve from cache without invoking the resolver.

html
<bext-include src="/__bext/nav"
  ttl="5m" swr="1h" tags="chrome"
></bext-include>

The nav is resolved once, served fresh for 5 minutes, then served stale for up to an hour while bext revalidates it in the background. Tagged chrome so you can invalidate all chrome fragments with a single purge call.

The attributes#

Attribute Format Purpose
ttl 5m, 1h, adaptive Cache freshness lifetime
swr 1h, 1d Stale-while-revalidate window
tags chrome,nav Tags for grouped invalidation

Duration syntax: 30s, 5m, 1h, 1d, or raw ms (e.g., 200ms). No ttl means no caching.

How caching works#

bext's include cache is a wrapper over the existing tiered cache:

code
Request arrives
  ↓
Compute cache key = hash(src, vary_values)
  ↓
L0 (hot map) → HIT: return cached HTML
           → MISS ↓
L1 (mmap)   → HIT: promote to L0, return
           → MISS ↓
L2 (Redis)  → HIT: promote to L0+L1, return
           → MISS ↓
Resolver chain → compute HTML
  ↓
Store in L0 + L1 + L2 with TTL
  ↓
Return HTML
  • L0 is a per-process DashMap — sub-microsecond reads.
  • L1 is mmap-backed and persists across restarts, so warm caches survive deploys.
  • L2 is Redis (optional) and shares state across horizontally-scaled instances.

Stale-while-revalidate#

With swr="1h":

code
time=0     include resolved, stored in cache (TTL=5m, SWR=1h)
time=4m    request → fresh → return cached
time=6m    request → stale but in SWR window
             → return cached HTML immediately (non-blocking)
             → spawn background task to revalidate
           background task completes → cache updated
time=1h5m  request → expired → cache miss → resolve

The user never waits for revalidation. The stampede guard ensures only one background revalidation runs per key, even under traffic spikes.

Cache tags#

Tags let you invalidate groups of includes at once:

html
<bext-include src="/__bext/nav" tags="chrome,nav"></bext-include>
<bext-include src="/_partials/footer.html" tags="chrome"></bext-include>

After deploying a new footer:

bash
curl -X POST http://localhost/__bext/cache/purge \
  -d '{"tags":["chrome"]}'

Both includes are invalidated. The next request rebuilds them.

Tags can also come from config:

toml
[[include.rules]]
pattern = "/__bext/nav*"
tags = ["chrome", "nav"]

[[include.rules]]
pattern = "/_partials/*"
tags = ["chrome"]

Adaptive TTL#

Use ttl="adaptive" to scale cache lifetime with hit count:

html
<bext-include src="/partials/homepage-hero"
  ttl="adaptive" swr="1h" tags="homepage"
></bext-include>

The formula (same as ISR adaptive TTL):

code
next_ttl = min(base_ttl * (1 + ln(1 + hits)), max_ttl)

With defaults (base=60s, max=600s, threshold=10 hits):

Hit count Effective TTL
< 10 60s (base)
100 ~337s
1000 ~475s
10000 600s (capped)
∞ 600s (capped)

Popular includes stay cached longer (better amortization of resolution cost). Cold includes expire at the base TTL (better freshness for the long tail).

Stampede protection#

When a hot include expires and a flood of concurrent requests arrive, only one worker regenerates — others either serve stale content (if in the SWR window) or wait for the first regeneration to complete.

rust
pub enum StampedeLock {
    Acquired(token),  // you regenerate
    Waiting,          // someone else is regenerating
}

This prevents the "thundering herd" problem where N workers all compute the same expensive fragment simultaneously.

Configuration#

Global cache tuning:

toml
[include.cache]
enabled = true
l1_max_entries = 5000
l2_prefix = "bext:include:"
max_variants_per_include = 100

[include.cache.adaptive]
base_ttl_ms = 60_000
max_ttl_ms = 600_000
extension_threshold = 10

Per-source defaults (applied to any matching include, including SSI/ESI):

toml
[[include.rules]]
pattern = "/__bext/nav*"
ttl = "5m"
swr = "1h"
tags = ["chrome"]

[[include.rules]]
pattern = "/api/*"
ttl = "30s"
swr = "5m"
timeout = "200ms"

[[include.rules]]
pattern = "/_partials/*"
ttl = "1d"
tags = ["chrome"]

The first matching rule wins. Attributes on the HTML element override the rule.

Tip

The first matching [[include.rules]] entry wins. Put more-specific patterns (e.g. /api/live/*) before broad catch-alls (e.g. /api/*) so they are not shadowed.

When not to cache#

Warning

Omitting ttl disables the cache entirely — the resolver runs on every request. For live data, prefer a short ttl plus swr, or use Realtime includes instead of relying on zero-TTL caching.

Some includes should resolve fresh on every request:

  • Per-user personalization without a vary dimension (you'd be caching one user's content for another).
  • Live data like current timestamps, cryptocurrency prices, stock quotes. Use realtime instead.
  • One-shot operations like CSRF tokens, nonces.

Omit ttl to bypass the cache entirely — the resolver runs on every request.

See also#

Edit this page ↗Need a hand? ↗
Search documentation

Start with a topic, a command, or a question.