Every query the engine runs is bounded twice: once while discovering which symbols answer the query, and once while assembling the response that carries them. This page states the law that keeps the two apart, because they fail differently: a discovery bound threatens correctness, an assembly bound only threatens completeness of a listing. The engine treats them accordingly — one warns and refuses to propagate, the other marks and paginates.

The page assumes Planning from Measured Cardinality: probes, anchors, and the refinement waves. Here we care about what happens when a bound is hit.

1. The two kinds of capping

Discovery capping is a correctness hazard. A statement resolves by reading rows — its own matches, or its neighbours’ parents and children. Every such read carries LIMIT k+1 where kk is the safety cap (--probe-cap, shared with the probe so “resolved” and “not truncated” name the same threshold). If the read returns k+1k+1 rows, the statement’s selection is capped: a truncated, order-arbitrary subset of the true answer. Three rules follow:

  1. A capped selection is flagged (Selection.capped) and the flag survives every path that copies or derives from it — including the derive path, where a receiver’s own filter can shrink the re-read below the cap and hide the truncation.
  2. A capped selection is not a participant in composition: it neither narrows its neighbours nor is narrowed by them. Constraining from an arbitrary subset would silently drop correct answers; the engine retries the read once a neighbour’s constraint makes it more selective, and composes only then.
  3. A capped statement is said: the result carries too many results: this part of the graph is incomplete, with advice and without totals — counting the true size would cost the very scan the cap exists to avoid.

Assembly capping is a pagination invitation. Once the symbol set is settled, the response bounds what each node carries: its instance list, its implicit edge listing, the call sites per edge. Hitting these caps sets markers (edges_truncated, instances_truncated + total_instances), never the capped flag and never a warning — the graph is right, a listing is merely paged, and the pagination endpoints serve the rest.

The boundary between the two is structural: assembly runs strictly after the composition fixpoint drains, so nothing capped-for-pagination can flow back into discovery.

2. The per-command result budget

The response itself is bounded by the result budget (--max-result-symbols, ?limit=, or limits(symbols=) per command): the maximum distinct symbols shipped per command. Per command, not per response — every selector is its own request with its own budget, so a dedicated anchor can never be evicted by a neighbouring command’s fan-out. (The global variant was tried and produced exactly that failure: an anchored symbol whose file path sorted after a large module’s derived neighbours lost the cap lottery to them.)

Within a command, survivors are chosen deterministically by (path, offset, id); a symbol kept by any command survives; truncated reports that some command overflowed. A kk-command query can therefore return up to k⋅limitk \cdot \text{limit} symbols, which stays small because per-node payload is independently bounded (§3) and kk is the query’s own size.

The budget also reaches into SQL: leaf reads of budget-bounded verbs are bounded by a multiple of the budget, so a bare enumeration cannot materialise far more rows than the response could ever keep.

3. Render-side assembly cap

One listing cap (--listing-cap, default 100; 0 disables) bounds the rows in every per-node listing — the three listings collapsed into one number after measuring that a uniform cap is safe (a hub at listing=100 is ~380 KB; the distinct-callee bound keeps the call-site factor from multiplying out):

  • instances per node. The engine’s instances were already bounded by the safety cap; only serialization was not — a single kernel module’s 931 instances exceeded the 1 MB ceiling on their own. Real code instances sort before sentinel/containment ones, so a truncated list can never misclassify a code symbol as a directory node or drop the definition a renderer looks for. total_instances is exact for truncated nodes (an index count, bounded by the symbol’s own size).
  • distinct out-edges per node. Fetched discovery-side as a windowed SQL LIMIT (row_number() OVER (PARTITION BY from_instance) with a +1 sentinel), sized to the largest listing any command requests; render trims each node to its own.
  • call-sites per (from, to) pair. A client draws one edge per pair; entries beyond the first only feed that pair’s call-site listing, and a single hot pair was measured carrying 2,454 of a response’s 2,898 edge entries. The edge set is hash-ordered, so entries are sorted before capping — otherwise the survivors would be iteration-order lottery.

Objects follow the same discipline: a file entry ships only if a kept occurrence or a kept instance references it.

The listing cap (like the symbols budget) is set per command by the limits() verb, which inherits into nested scopes; the server flag is only the default.

4. The pagination channel

What assembly withheld, two endpoints recover per node (HTTP API):

  • GET /v1/symbol/{id}/references pages the references out of a symbol in the engine’s own (from_instance, ref) unit — the same containment rule and target choice edge discovery uses, so fetched rows are exactly the withheld edges. The page query is built to stream: source instances walked in id order, each one’s references via a lateral in ref-id order, so the plan is an incremental sort that stops at limit+1 rows — 3 ms per page on a symbol with 931 instances and a million references, where the naive DISTINCT ON form took seconds and a 65 MB disk sort.
  • GET /v1/symbol/{id}/instances pages a symbol’s instances by id keyset with an exact total.

Cursors are keyset, opaque, and honest about scan position: a row the response filters out (a self-reference has no target instance and the engine draws no such edge) still advances the cursor, or a page boundary landing on one would end pagination early with rows remaining.

Counts differ by kind, deliberately. An instance total is exact — one index-driven count bounded by the symbol’s own size. An edge count is capped (“1000+”): exact out-edge totals for a hub node cost the unbounded scan this whole design eliminates.

5. Truncation and the caches

A capped discovery read that populates a layer persists its truncated bit on the layer row, and the warning is reconstructed from that bit on every cache hit — a cached truncated result warns exactly like a fresh one (see Search § Truncation semantics). The SQL result cache is unaffected by assembly caps: pagination pages bypass it entirely, since a page is poor cache currency.

6. Configuration

FlagEnvDefaultBounds
--probe-capASKL_PROBE_CAP1000discovery reads and probes (§1); 0 disables resolution
--max-result-symbolsASKL_MAX_RESULT_SYMBOLS100distinct symbols per command (§2)
--listing-capASKL_LISTING_CAP100rows per per-node listing: instances, out-edges, call sites per pair (§3)

0 disables the corresponding cap. Per request, ?limit= (symbols) and ?listing= override for one response; per command, the limits(symbols=N, listing=M) verb overrides and inherits into nested scopes.

Where to read more