Feeds — Caching
Feed pages are cacheable: same (lens, feedName, context, cursor, pageSize)
tuple returns the same response within a short window (until the underlying
data evolves). The client does the caching — you BYO the storage backend.
@semilayer/client exposes a FeedCache interface and five built-in
adapters. The React hook threads cache hits through automatically.
The interface
Keys are deterministic hashes — the client builds them. Values are JSON
strings of FeedPage. TTL is suggested, not enforced by the client.
Built-in adapters
| Adapter | Scope | When to reach for it |
|---|---|---|
inMemoryFeedCache({ max }) | Per-tab, per-session | Default. Fast, simple, no persistence. |
localStorageFeedCache() | Per-origin, persistent | Long-scroll feeds you want to keep between visits. |
sessionStorageFeedCache() | Per-tab, persistent until close | Per-tab history without leaking across tabs. |
indexedDbFeedCache() | Per-origin, persistent, large | Heavy feeds with many pages (>1MB of cached data). |
nodeFsFeedCache({ dir }) | Per-process, persistent | Server-side rendering / Beam running in Node. |
Per-call override
You can pass a cache instance per call — useful when a single feed wants different persistence than the client's default:
React hook
useFeed accepts a cache option that the hook uses for all its page
fetches:
The onCacheMetric callback fires once per cache interaction — useful for
wiring real-time hit-rate telemetry without touching the adapter.
Custom adapters
Implement the four-method interface against any store you like:
Drop into new BeamClient({ feedCache: redisFeedCache(redis) }). Works for
server-side feed pipelines, SSR with a shared Redis, edge deployments — any
store with get/set/del.
TTLs and invalidation
The client sets TTL based on the feed's evolve.minNotifyInterval — a feed
with ticks every 30s uses a 30-second TTL. For feeds without pollOnIngest,
TTL defaults to 5 minutes.
You can clear manually when your side of the world changes:
useFeed's refetch() forces a network call regardless of cache state.
What doesn't get cached
- Subscribe streams — live ticks go through the WebSocket, never through the cache.
- Explain responses — single-record, sk-only, not worth caching.
- Non-200 responses — errors are never cached.
Server-side context cache
Separate from the client cache is a server-side context cache that
stores embedded text results (e.g. the vector for liked_titles: [...]).
That's transparent to clients and tunable with four env vars on the service:
FEED_CONTEXT_CACHE_MAX, _TTL_MS, _PER_ORG_MIN, _PER_ORG_MAX_PCT.
Admins can watch hit rates at /admin/feed-cache/stats in the Console.
That cache is the reason liked_titles doesn't re-embed on every request —
same text, same pod, one API call regardless of caller volume.