Skip to main content
When a document that other answers read changes, Brussle judges each affected answer again, once, and no person confirms the work. You confirm the most it can cost one time, when you create the judgment, and the rolling limit holds it there. This work is a fan-out. It is how Brussle runs exactly the answers that a changed record affects, and it is the main cost of a judgment that reads a document other documents share. This page shows how to estimate that cost and how to keep it small.

How fan-out works

A fan-out starts when a change moves what a relation shows from a document that many judged documents read: Brussle judges again each judged document that reads the changed document and is inside the judgment’s re-judge scope. Two numbers set the cost:
re-judgments a month = changes a month × judged documents per change inside the scope
One 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_change fans out. Under periodic and manual, the change makes the answers in scope stale. Under on_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_ms still 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 the answers show covers how each answer reads while its fan-out runs, waits or leaves it out.

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 a PATCH, with no new version. The judgment’s controls are in freshness.fanout. share is a namespace setting.
A 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 original created_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 stale with stale_reason referenced_changed until something judges the document again.
When other judgments read its answers. A judged document outside the scope keeps an old answer, and a judgment that reads that answer would keep reading it, with nothing to update it. A judgment whose answers other judgments read therefore needs "scope": {"created_within": null} and no where.
  • Brussle refuses a create that would read a judgment with a scope, with invalid_request naming 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 PATCH that gives it a scope again, with conflict naming 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 stale with stale_reason limit_reached, and the change in their referenced_changes has deferred: true. The events feed records one judgment.limit_reached event for the change. Its deferred is 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_at says when the window next frees room. A PATCH that raises rolling_limit starts 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, then fresh, and fanout_window.used rises by each re-judge as it lands. Subscribe a webhook endpoint to judgment.* to learn of a deferral when it happens.
  • A block past block_cap waits as a whole for a higher cap or a finer key, whatever the window. Its judgment.limit_reached event has limit: "block_cap" and names the key value. At create, Brussle refuses a key value whose block is already past the cap, with invalid_request.
  • No limit. "freshness": {"fanout": {"rolling_limit": null}}, at create or in a PATCH, re-judges every document in scope on every change, with no deferral. cost_usd_at_limit is then null, 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 under on_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:
  • share is 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. GET returns 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.