What “budget” means here
Money, or load on a machine somebody pays for:--budget, budget.ceiling and
budget.session_ceiling. Two unrelated things are also called a budget and are
not governed here, because they protect agent context rather than spend:
query.max_payload_bytes and the semantic catalog’s caps. --confirm likewise
carries a second meaning on the project commands, where it overwrites a stored
plan or resolves an apply conflict; that use is not a cost gate, and it lives in
�5.
The handshake
cost is a preflight estimate, and it comes from free dry-runs. Any command
that would spend returns needs_confirmation unless given --confirm, plus a
--budget on billed connectors; DuckDB is free, so the confirm handshake alone
gates it. Re-issue the same command with --confirm and --budget <magnitude>
in the connector’s unit, and the confirmed run re-checks every statement against
the budget with a server-side cap as backstop. An estimate over the ceiling is
refused outright; confirmation cannot override it.
transform build --verify prices its row counts into the same estimate as the
build itself, as a (row counts) entry in the per-table breakdown, so one
--budget covers both phases. Only a relation the warehouse keeps no row count
for costs anything, which is any view; a table’s count is free metadata, and a
verdict resting on it is reported exact: false to say so. On a cold dev target
the relations to count do not exist before the build writes them, so the counts
are priced again afterwards as a phase drawn against the reservation the build
is already holding; where the estimator cannot price them upfront at all, a note
says so.
A priced phase the caller did not request is an offer, not a refusal. When a
command’s free half is a complete answer in its own right, the envelope is ok
and the price of the optional half sits in data.offer, carrying the same
estimate, breakdown and hint a refusal would. maintain check and
maintain semantic are the two. cost.estimate stays unset on an offer, so a
populated cost.estimate on an ok still means settled preflight for work that
ran, and needs_confirmation stays reserved for work the caller asked for and
has not authorized.
Paradigms
cost.paradigm names the connector the command ran against, not what the
command happened to cost, so a free metadata command still reports the paradigm
a billed one would bill in. free_local is a positive claim that the connector
bills nothing; null means no connector was resolved.
What the envelope carries alongside the paradigm, including
estimate_quality
and the data.spend key parity rule, is in
�2.
The per-command ceiling and --scope
--budget sets the ceiling for one command and is refused when missing, because
nothing runs unbudgeted. --scope narrows the committed source allowlist for
one command, in each connector’s own namespace vocabulary. Two rules make it a
cost control rather than a hint: a committed allowlist is a cost boundary, so
every --scope entry must resolve inside it and one reaching outside is
refused; and a scope is honored or named in an error, never dropped, so an entry
that names nothing refuses and lists what exists. It is never written back to
config. The per-connector vocabulary is in
�3.
Pricing an auto-profile
explore query and explore cluster profile an object they name that the
connection has but the .dex/ cache cannot adjudicate, because the firewall
cannot judge a column whose flags it does not have. That scan is billed, and it
is priced into the same handshake as the statements rather than added
afterward, so the estimate you confirm is the whole cost. A call carrying
several statements is quoted once for all of them, itemized per statement, and
an object two of them share is scanned once rather than twice. Resolving which
objects need it stays free: it is object listing and column metadata, the same
reads the inventory uses. explore cluster prices its profile first and gates
its sample mid-command, so a budget too small for the sample returns
needs_confirmation with the profile already saved rather than discarding it.
The spend ledger
.dex/spend.jsonl is one JSON object per line, appended and never rewritten.
Every row carries the same keys, null where one does not apply, so the file
parses to a stable schema and no reader has to interpret an absent key.
Settled spend is the
entry == "settlement" filter, summed over one
connector’s unit. That is not the same number as session_spent_today whenever
a command is in flight: a reservation is positive and its release is the same
magnitude negative, so the three kinds net to actual spend once a command has
settled, and until then the day’s total legitimately reads higher by the
headroom being held. Sum what you are given; clamping the negative would leave a
release uncancelled.
A ledger holding settlements alone is not a ledger missing rows. A
reservation exists to be seen by a concurrent command settling against
budget.session_ceiling, so a project that never set a daily cap has nothing to
protect and writes none. A row with no entry at all was written by a dex
older than v1.5.1 and is a settlement; filter on entry being "settlement" or
absent to include them.
The ledger is a dependency of billing, not of every command. Billed
admission reads it and fails closed if it cannot, with a named refusal
(reason: guard) that says nothing ran, so re-issuing is safe. Settlement
tolerates a read failure instead, so a backend that goes away mid-command does
not turn a command that already ran into a refusal. Writing a store that serves
this contract is �1.
The cumulative ceiling
budget.session_ceiling binds per UTC day and across commands that overlap in
time, not only across commands that follow one another. An admitted command
books its estimate against the day’s headroom before it runs and releases the
unspent part when it settles, so a second command issued while the first is
running is measured against what is genuinely left. Three consequences a caller
can see:
cost.ceilingon a refusal reflects headroom another command is holding, so two runs of the same command can be refused against different numbers.session_spent_todaycounts held headroom, so it reads higher than settled spend while another billed command runs and can briefly exceed the ceiling without anything having overspent. Run commands one at a time and it is exactly settled spend.- A command killed outright leaves its estimate booked until the UTC rollover. Every softer exit, including an interrupt, releases.
budget.ceiling, a missing budget.session_ceiling only warns, because
refusing would break every project that never set one. Config is read from
<repo_root>/.dex/config.yml and does not inherit, so a second repo root has its
own budget or none.
The one-time ask
That warning is accurate and it is also the default state of every new project, so on its own it would repeat on every billed command, which is the condition under which warnings stop being read. Instead, the first billed command in a project with no recorded decision returnsneeds_confirmation naming a
suggested_session_ceiling (five times that command’s own estimate, in the
connector’s unit, as a starting point) and a session_ceiling_hint spelling out
both answers. Answer with --session-ceiling <value> to set one or
--no-session-ceiling to record that the project runs unbounded. Either answer
is written to .dex/config.yml and nothing asks again in that project. The
answer is an in-place edit of the budget lines, so the file’s comments and key
order survive it, and it is reported as an update diff. To set or change the
ceiling outside the ask, dex config set budget.session_ceiling <value> proposes
the same edit as a diff and writes it on --confirm.
dex config unset budget.session_ceiling removes it the same way, and the next
billed command asks again.
The ask is the last check before spend, so an unanswered one has run nothing,
booked no headroom, and never reached the ledger. The unconfirmed cost ask that
precedes it carries the suggestion in notes, so one re-run can answer both.
Three cases are never asked: a project that already set the ceiling, one that
recorded a decline, and a config-free ad-hoc read, which has no committed file
to record an answer in. A decline loosens nothing: the warning still fires and
now names the decline, so a reader can tell a settled choice from a project that
was never asked.
The one surface with no cost guard
The hosted dbt Cloud Semantic Layer (explore semantic query --api) is the one
place dex cannot enforce a ceiling. dbt Cloud owns the warehouse connection and
executes the query server-side, so no dry-run estimate and no server-side cap
are possible from dex. That backend therefore does not ask for --confirm,
because a confirmation dex could not back with a ceiling would be dishonest. It
reports cost.paradigm: hosted with no estimate and no ceiling, and states on
every result that the cost guard is unavailable and spend is governed by the dbt
Cloud environment. Do not present a hosted result as cost-guarded, and do not
read the absence of an estimate as “it was free”.
The local backend (--local) executes through dex’s own connector and keeps the
full handshake. Which axis decides this is in
�1.
Source: references/cost-controls.md (edit on main). This page is generated; change the source, not the site.