Change one model. Rebuild only what it reaches.
Freshet reads your SQL models and answers one question: what must rebuild? It follows lineage to every model a change reaches, and narrows incremental ones to the partitions that moved. On 10,240 models it answers in about a second. It never runs a query. Your orchestrator does that.
Two changes land: late rows restated into events for
2026-06-20, and accounts reformatted. Models shown in topological order.
-

accounts
table
Reformat only
Whitespace and casing changed; the syntax tree didn't. The fingerprint holds, so nothing below it rebuilds.
0 CU
-


events
incremental · day on
dt1 partition
Restated for 2026-06-20:
insert_overwriteover that one partition.0.100 CU
-

plans
table
Unchanged
Nothing it reads changed.
0 CU
-

fx_
rates view
Unchanged
Nothing it reads changed.
0 CU
-


daily_
rollup incremental · merge on
dt1 partition
Downstream of
eventsvia[dt], so the same single day.0.100 CU
-

account_
mrr table
Unchanged
Reads
accounts, whose fingerprint held. A file checksum would have rebuilt it.0 CU
-

invoices
table
Unchanged
Reads
accountsandfx_rates. Neither changed.0 CU
-


user_
dim table
Full refresh
Downstream of
eventsvia[last_seen, user_id]. A table has no partitions to replace, so the plan widens it and says why.1.000 CU
How Freshet decides
The shelf above is a real plan shape. This is the command that prints it, and the three rules behind every line.
- Cosmetic edits never rebuild
- Freshet parses each model and hashes its syntax tree, not its text. Reformat, re-case or re-comment a model and its fingerprint doesn't move.
- Lineage down to the column
- Every rebuild names the columns that carry it:
user_dimis downstream ofeventsthrough[last_seen, user_id]. Pointed at a dbt project,freshet explaindiffs column by column and skips the models that read none of the changed columns. In 0.1.0,freshet plantreats a code change as touching every column of the model it changes. - Partitions first. Widen, never narrow
- Incremental models replace only the partitions that moved, using lookback windows, grains and watermarks. When a replacement can't be honoured, the plan widens it to a full refresh and prints the reason. A narrowed write would leave stale rows, and nothing downstream would catch it.
$ freshet restate --project ./models --model events --partitions 2026-06-20
backfill plan (3 model(s), topological order):
events partitions[2026-06-20] (directly changed)
└─ writes: insert_overwrite over 1 partition(s) on `dt`
daily_rollup partitions[2026-06-20] (downstream of events via [dt])
└─ writes: merge over 1 partition(s) on key [dt]
user_dim full (downstream of events via [last_seen,user_id])
└─ writes: full_refresh over the whole relation
1 model(s) widened to a full refresh of the whole relation:
user_dim the model is a table, so it has no partitions to replace in place
--sql postgres, snowflake, bigquery, databricks
or redshift and the plan also carries the statements for your runner to execute.The firing record
We exported one generated project of 10,240 models as dbt, SQLMesh and Freshet. Each tool ran its own command as a whole process: one warm-up, five timed runs, median shown. We compared times only where the answers matched. For the one-model edit, every tool picked the same 80 models.
What must rebuild after m_0_0 gains a WHERE? Every tool answers 80.
- Freshet 0.1.0 1.039 s plan
- dbt v2 2.0.5 12.865 s 12× slower
- dbt-core 1.12.5 80.322 s 77× slower
- SQLMesh 0.236.2 486.657 s 468× slower
Linear scale: the longest run in each question fills the row.
| Question | Freshet 0.1.0 | dbt-core 1.12.5 | dbt v2 2.0.5 beta | SQLMesh 0.236.2 |
|---|---|---|---|---|
| Parse every model, build the DAG | 0.810 | 73.577 | 10.201 | 101.317 |
| Same, cache warm | 0.333* | 12.430 | 1.213 | 75.460 |
| Nothing changed (all answer 0) | 1.113 | 77.805 | 13.023 | 181.049 |
| One model edited (all answer 80) | 1.039 | 80.322 | 12.865 | 486.657 |
| Nothing built yet (all answer N) | 1.354 | no such step | no such step | 521.904 |
* A curl to a running freshet-serve that parsed the project once at
start-up. A long-running daemon is a different architecture from the others' per-run caches.
Freshet's CLI keeps no parse cache, so its other rows are cold.
Same answer, different work. Freshet's plan also derives column lineage,
fingerprints every model's syntax tree and prices the plan. dbt ls compares file checksums
and selects descendants. SQLMesh's plan --explain renders every query and computes
intervals. What they share is the answer you wait for.
Current code, re-timed. Run A predates Freshet's lineage fixes. Run B re-timed
Freshet at 7e68f20 with dbt v2 as a control: plan took 0.823 s against dbt v2's 10.535 s,
and the warm daemon replan 0.295 s. Run B's machine was faster, and the control moved by as much as
Freshet did, so we don't mix runs. In-process, a warm replan on 10,240 models is 30–37 ms on one core.
State the whole team can commit to
A state.json in git is last-writer-wins. When two CI jobs plan at once, the second
commit silently overwrites the first one's record of what was rebuilt. The next plan then
under-rebuilds, and nobody notices.
Point --state at hosted state instead. Every commit is a compare-and-swap against the
version the plan was computed from. If another commit landed in between, nothing is written:
Freshet exits 3 and tells you to re-plan.
submit never commits state. It hands the plan to your runner, and you commit only after
the run has succeeded. A --budget caps a plan at N compute units, reports the headroom
left, and fails the pull request whose migration would rewrite the warehouse.
$ export FRESHET_STATE_TOKEN=…
$ freshet plan --project ./models --state https://…/state/v1 --json > plan.json
hosted state: version 7 at https://…/state/v1
# … run plan.json; once it has succeeded:
$ freshet plan --project ./models --state https://…/state/v1 \
--commit --expect-version 7
hosted state at https://…/state/v1 moved: this plan was computed
against version 7, and the state is now at version 8 — another
commit landed after this plan was computed; re-plan. Nothing was
committed.
$ echo $?
3
Plan for free. Pay for the state you share.
Planning costs milliseconds of one core, so no edition meters plans. Replan on every pull request. The planner is the same everywhere. What changes is who runs the state it commits to.
-

dagron Cloud
Hosted state, operated for you.
Operated
- Hosted state at
/state/v1, with compare-and-swap commits for every CI job and engineer - Backed up, migrated and on call: we run the Postgres behind it
- Run rebuilds on your own compute, or on dagron's workers metered in compute units. Column and partition pruning shrink that bill.
freshet submitstraight into a dagron run graph- Pay as you go: a platform fee plus usage. No seat or project caps.
Start on dagron CloudWe reply with a workspace and a
FRESHET_STATE_TOKEN. - Hosted state at
-

Self-hosted
The same state backend, on your infrastructure.
Your network
- A multi-tenant state backend you run yourself, behind your own boundary
- Support from the team that builds the planner
- Bring your own compute
-

Open source
The whole planner, uncapped.
Apache-2.0
- Every surface above, with nothing gated
- State in a
state.jsonyou commit to git --budgetas a guardrail you set for yourself, not a paywall