How fan-out works
A fan-out starts when a change moves what a relation shows from a document that many judged documents read:- a referenced document, such as the product that many order lines name
- a document in a named list
- a block of documents that share a key, and the link or match a matcher made in it
- the answers of another judgment that a relation reads
- the outcomes you post for related documents
re-judgments a month = changes a month × judged documents per change inside the scopeOne edit to a referenced document that 200,000 judged documents name can cause 200,000 evaluations. Each control makes one of the two numbers smaller, or sets a maximum for their product.
- Only a change to what the relation shows counts. An edit to a field that the relation does not show, or a move inside a band, starts nothing and marks nothing
pending. - Only
on_changefans out. Underperiodicandmanual, the change makes the answers in scopestale. Underon_read, the next get or query of an answer judges it again. - A fan-out has its own debounce. It waits until the changed document settles, 10 minutes by default. The judgment’s own
debounce_msstill applies to writes to the judged documents, so a new order line is judged within seconds. - A change to a judged document’s own related documents is not a fan-out. A new amendment re-judges only its contract. See related documents.
What a fan-out costs
The examples use one namespace:- It has 100,000 referenced documents. On average, 200 judged documents point at each one: 20M judged documents.
- 4% of the judged documents were created in the last 30 days, which is 8 for each referenced document. A quarter of those 8 agree with the scope’s
where, such as an open status: 2. - The context and question of each judged document are about 900 tokens, so each evaluation is a standard judgment. Pricing turns re-judgments into a price.
The scope makes B’s judged documents per change 100 times smaller, and bands make its changes 30 times smaller. Together they bring B down to the size of A. Without bands, B with the scope and
where is 6,000,000 re-judgments a month, twenty times the default rolling limit. Brussle defers what goes past the limit. Raise the limit, or add bands.
The debounce and its ceiling hold a field that changes much more often. A number that you write each minute is 43,200 changes a month for each referenced document. The 10-minute fan-out debounce never ends for it, so the 1-hour ceiling fans out once an hour while the number keeps changing: 720 times a month. With bands, only a change of band counts. A band change that then holds causes one fan-out. A band that keeps changing fans out once an hour.
A large referenced document can have 200,000 judged documents that point at it. One change fans out to all of them and counts 200,000 against the rolling limit. With the 30-day scope and where, it is about 2,000.
Judged documents outside the scope cost nothing. Brussle bills nothing for a judged document whose context did not change.
The controls, in order of effect
Change each default with aPATCH, with no new version. The judgment’s controls are in freshness.fanout. share is a namespace setting.
PATCH like this one changes those two keys and keeps the others, scope.where too. GET returns each value in effect, and fanout_sources says where each one comes from: judgment or default. You do not confirm a wider scope or a higher limit, though either can make the bill much higher. The namespace budget still sets the maximum spend. Freshness policies lists each setting with its bounds.
Set the scope
- It counts from the change, not from now. The scope of one change stays the same however long its fan-out waits.
- Age comes from
created_at: when Brussle first got the document, or the creation time that the write gave. Import judged documents with their originalcreated_at, and only the recent ones are in scope. Imported without it, each one is in the default scope for 30 days. - Outside the scope, an answer keeps its numbers and reads
stalewithstale_reasonreferenced_changeduntil something judges the document again.
"scope": {"created_within": null} and no where.
- Brussle refuses a create that would read a judgment with a scope, with
invalid_requestnaming the path. - With no scope, each change re-judges every judged document that reads the changed document. Bound the judged documents with
applies_to, such as only open orders, not with the scope. - While the judgment has readers, Brussle refuses a
PATCHthat gives it a scope again, withconflictnaming the readers.
The rolling limit
freshness.fanout.rolling_limit is the most documents a judgment judges again in any 30 days because of changes outside the document itself: its referenced documents, its blocks, and the answers and outcomes it reads. Every judgment with a relation has one, roll-ups included. The default is 300,000. You confirm it once, at create, priced in full as cost_usd_at_limit. After that no fan-out waits for a person.
- What counts. Each re-judge counts once, when its answer lands, even when it needs no engine call. A retried re-judge counts once.
- Within the limit, a fan-out runs at once, with no job. Its spend shows in the namespace’s spend.
- As much as fits runs. When a change needs more re-judges than the window has room for, Brussle runs as many as fit, at most 1,000 at a time, and defers the rest. A large upload of outcomes runs the same way.
- Deferred, never declined. The answers still to judge read
stalewithstale_reasonlimit_reached, and the change in theirreferenced_changeshasdeferred: true. The events feed records onejudgment.limit_reachedevent for the change. Itsdeferredis the number of judged documents still to judge:
- Catching up. Deferred work runs by itself as older re-judges leave the 30-day window;
fanout_window.rolls_atsays when the window next frees room. APATCHthat raisesrolling_limitstarts it without waiting, usually within a minute, and needs no confirm. A change larger than the whole limit still makes progress month by month. - Watch it. No event marks the catch-up. Each deferred answer reads
stale(limit_reached) until it is judged, thenfresh, andfanout_window.usedrises by each re-judge as it lands. Subscribe a webhook endpoint tojudgment.*to learn of a deferral when it happens. - A block past
block_capwaits as a whole for a higher cap or a finer key, whatever the window. Itsjudgment.limit_reachedevent haslimit: "block_cap"and names the key value. At create, Brussle refuses a key value whose block is already past the cap, withinvalid_request. - No limit.
"freshness": {"fanout": {"rolling_limit": null}}, at create or in aPATCH, re-judges every document in scope on every change, with no deferral.cost_usd_at_limitis thennull, and only the namespace’s budget limits what fan-out spends. Without bands, a field that changes often then re-judges everything in scope each time. A higher limit with a known cost is safer.
What the replay estimate cannot count
Before a judgment with a relation runs underon_change, its create returns the replay estimate of the monthly cost and creates nothing until you send confirm: true. A change of its policy to on_change does the same.
The replay cannot count fan-out: it cannot tell which past writes to a referenced document changed what the relation shows. For such a judgment it returns "excludes": ["fanout"] and lower_bound: true, and the dashboard shows the figures as “at least, not counting fan-out”. It still counts the writes to the judged documents themselves.
The rolling limit sets the most fan-out can add. The response gives its price beside replay, in fanout, at the sample’s judgments for each answer and your organization’s tiers:
cost_usd_at_limit is the most fan-out can cost in any 30 days, and you confirm it with confirm: true. Here, 300,000 standard judgments at the first tier are $75. With "rolling_limit": null in the create, both values are null. A judgment in a chain also gets graph_ceiling, the same bound summed over every judgment that reads it. See roll-ups.
Namespace fan-out settings
PATCH /namespaces/{ns} sets how fan-out runs for every judgment in the namespace:
shareis the most of the namespace’s engine requests in progress that fan-out can use. It is more than 0 and at most 1, and the default is 0.5. Fan-out always gets at least one request, and the rest do the namespace’s usual judging.GETreturns the value in effect. See namespaces.- Fan-out is background work. It runs at a lower priority than the namespace’s own changes, like a backfill.
- A budget pause stops fan-out, as it stops all judging.
Not available yet
- Fan-out in the replay estimate.
- A limit on the total fan-out of a namespace, other than its budget and each judgment’s rolling limit.