5.13 Timely sources — cache: currency and the freshness result block
A producer’s cache: declares how CURRENT its answers are:
cache: {kind: fresh}(an alias ofkind: none, the default) declares a timely source: every query re-reads the source, so the answer is the current state as of the query — re-running the same query re-observes the source. This is the declaration behind “what is the current X, joined to my graph”, and a scheduled re-run of such a query is a fresh observation each time. Within ONE query the same read is made once: a fresh source asked for the same keys by two joins, or by the engine’s own pre-pass and its fetch, is read once and the records reused, and the envelope bills one call. Independent fresh sources in the same query are read concurrently (their cost is the slowest, not the sum), exactly as cached ones are.cache: {kind: ttl, seconds: N}declares that an answer up toNseconds old is acceptable: within the window the same lookup is served without re-reading the source; past it, the next query re-reads. A query may force one live re-read of a TTL source with{ai: {fresh: true}}on the virtual edge; the fresh result then serves subsequent queries within a new window.
One read is shared, not repeated — including by queries running at the same instant. A cold
lookup under ttl or immutable is read ONCE however many queries want it: the first to arrive
reads the source, and every query that wants the same lookup while that read is outstanding is
served its records rather than making a read of its own. This is the shape of every cold start and
every expiry under load — a view refreshed on a schedule, a dashboard opened by a team at once, a
handler fanning out — and for a source with a hard quota it is the difference between one call and
one call per asker. What each query says it spent stays honest: the query that read the source
bills the call, the queries served by it bill none and report the lookup as cached, observed when
that read happened — so cached can mean “read a moment ago, by someone else” and never means only
“old”. Three bounds on the sharing, each of which a realm can rely on:
- A failed read is never shared. “Could not ask” is one query’s verdict on one attempt, so a query whose share did not arrive asks the source for itself rather than inheriting a refusal it never made — and nothing is remembered either way.
- Nothing is shared across owners. A lookup is shared only between queries that would have been served the same cached entry: the same source, the same declaration, the same key, the same caller scope. One owner’s outstanding read never answers another’s question, exactly as one owner’s cached entry never does.
{ai: {fresh: true}}and a live-only read share in neither direction. A query that asked to read the source NOW is not served somebody else’s read of a moment ago, and its own read is not offered to anyone else as a share.
cache: {kind: none} (the default) shares nothing between queries, by declaration; within one
query it is already read once (above). A date-addressable source (periods:) caches per
(key, period) as always, but concurrent queries may each read a period that is still open — the
period fan-out is not covered by the sharing above.
What the cache is keyed on, and what a cached entry may answer. A cached fetch is keyed per
anchor key AND per what the fetch asked its source for: the filter the producer’s pushdown:
rules absorbed and rendered into the request, and the LIMIT the planner could push into the page
walk. Which predicates are considered at all is the attachment question (§7.6.1 lists the shapes; a
WITH does not end attachment); which of them reach the key is this one, and the two sets differ by
exactly the rules the door declares. A predicate no rule absorbs — a comparison through toLower, a
property with no rule — and a LIMIT the planner could not push leave the fetch byte-identical, so
they are no part of the key and the graph applies them to cached records exactly as to fresh ones.
The exact key is the first question the cache is asked, not the last: when it misses, a live,
complete entry for the same anchor key that answers a broader question than the one asked
serves it, with no call. A complete read under the same filter serves any shorter LIMIT and an
existence test; a complete read with no filter serves a question with a pushed filter, since what
the source would have returned is a subset of what it already did (a capped unfiltered prefix does
not, since the filtered records may lie past its cap); a cached LIMIT 50 prefix serves
LIMIT 10 in the same declared order — sound only because an early stop is licensed only in the
source’s declared order, so a different ORDER BY or a longer LIMIT is a fresh fetch. Whatever
served it, the graph applies the query’s own filter and LIMIT to the records exactly as it does
to a fresh fetch, and the result reports the entry as cached, observed when the broad read was.
The converse never holds: a narrower entry never serves a broader question; a truncated or partial
entry, and a one-record-per-key existence entry, serve nothing but themselves; and only producers
whose source returns a fixed collection narrowed by a pushed filter and a cap take part — a result
a demand or a model shapes (generative, aggregate, extraction) is keyed exactly, as is each window
of a periods: door.
What a family of views over one door costs, and how to make it share a read. Within one TTL
window the calls a family makes depend on the order it runs in: the broadest read first, and the
rest are served. No SPELLING changes that — a predicate written over a name a WITH introduced is
the predicate on the property it names and is pushed exactly as that property’s is (§9) — so views
over different windows of one door pay one read per window however they are written. Three things a
family can do instead, each measured on the shipping engine (2026-10-17):
- Let the broad view read first. A complete unfiltered entry holds every record a pushed window would return, so every window after it is served with no call, in any order among them: one read for the whole family. The converse costs what it always did — a window read first serves neither the broad question nor a wider window, and each pays its own call.
- Shape a terminal result with a list slice (
ORDER BY … WITH collect(row) AS rows UNWIND rows[0..$limit] AS row) rather than aLIMITthe planner can push, which leaves a prefix and not the collection; and keep aWITH … LIMITfor the place it earns its key, directly before a hop, where it bounds the hop’s anchors (README, “Predicate pushdown”). - Read it through a door declared
readWhole: true. Sharing is a property of the request, so a door that sends no filter is read once per key and serves every view of it in ANY order, narrowest first included.readWhole: trueis how a door says so while keeping itspushdown:rules declared: every read asks the source for the key’s whole collection, the rules are not sent, and the graph applies every filter (§9). That is also why aperiods:door’s history (§5.17) is free after the first read: its window is a key, not a narrowing. The price is the honest one — the whole collection crosses the wire once, and each read says the graph applied the filter.
A family whose reads the source narrows has no order-independent sharing: each read is keyed by the
filter it sent. Declaring the door readWhole: true is the way to ask for it, and it only pays with a
ttl or immutable cache to hold the read for the next view. Keep the rules on the read-whole door so
that a second, narrow door over the same operation can send them. pushdown: [] reads the same way but
states that the source cannot filter; on a source that can, it makes every read’s cost note untrue.
The freshness block. Whenever a query touched an external source, its result envelope carries
a freshness array alongside rows and warnings: one entry per source read,
{producer, source, observedAt, keys}, where source is "live" (read during this query) or
"cached" (served within its TTL window) and observedAt is the ISO-8601 time the source was
actually read — for a cached entry, the original read, never the query time. A join may mix the
two (a live anchor layer joined to TTL-served enrichment); each layer reports its own observation.
Failed reads never appear in freshness — they are reported in warnings. Purely graph-resident
answers omit the block.

