How DiffCost works
What DiffCost compares, how it turns bytes into dollars, what structural analysis looks for, and how accurate the numbers are.
What happens on each pull request#
-
Find the changes. DiffCost lists the SQL files (or dbt models) the pull request (PR) changes with
git diff <base>...HEAD, and checks that the checkout contains the PR's head commit. The base is the commit the PR is compared against. DiffCost uses:base_ref, if you set it;- otherwise, with the default checkout (GitHub's merge commit), the base-branch commit that the merge commit was built on: the base branch's tip at the PR's latest push;
- otherwise, the pull request's base commit (
pull_request.base.sha).
On a merge-queue run, the base is
base_refif you set it, otherwise the commit the queue's commit was built on (merge_group.base_sha). The checked-out commit must be the queue's commit (the default ofactions/checkouton amerge_groupevent). -
Read both versions. The base version of each file comes from git, the PR version from the checkout. For dbt, both versions are the compiled SQL from the two manifests (see dbt projects).
-
Dry-run both versions in BigQuery, in your Google Cloud project. A dry run is free and returns the bytes the query would scan, not rows.
-
Price the difference per run (see below), and parse both versions to flag structural risks.
-
Send the numbers (only with an API key) to the DiffCost service, which applies your thresholds, posts the
diffcost/cost-checkcommit status and returns the verdict. See Merge blocking. -
Post the PR comment (or update DiffCost's earlier one) once, with that verdict, and write the same report to the job summary. A merge-queue run writes only the job summary.
If DiffCost can't run at all (the base can't be read, the checkout isn't the PR's code, an invalid manifest_path, config_path or base_ref, a missing or untrusted dbt manifest), it says "DiffCost could not analyze this PR" with the reason and what to fix. It never reports that as "no changes".
How costs are calculated#
Every dollar figure is the cost of one run of the query:
- On-demand pricing: dollars = bytes ÷ 2^40 × $/TiB. The default is BigQuery's list price, $6.25 per TiB. Change it with the dashboard's Cost per TiB (with an API key) or the
cost_per_tbinput. BigQuery's 10 MiB per-table minimum and the free 1 TiB per month are not modeled. - Editions (slot-based) pricing: slot cost depends on your reservation, so the comment shows bytes scanned and a scan multiplier (for example
4.6x scan) instead of dollars. Without an API key, setbq_pricing_model: editions(input or.diffcost.yml). With an API key, choose Editions (slot-based) under Pricing model in the dashboard's BigQuery pricing settings: that setting always wins. Dollar thresholds still compare the on-demand equivalent at your Cost per TiB rate. - Monthly projection: the "≈$X/mo" figure is per-run cost × runs per month, 30 unless you set
default_runs_per_monthin.diffcost.yml. It's for context only: thresholds always use per-run numbers.
For a dbt incremental model, DiffCost prices the SQL that dbt compile produced, which is usually the incremental query when the model's table already exists.
Accuracy#
DiffCost uses BigQuery's dry-run API, which returns totalBytesProcessed without running the query.
| Scenario | Accuracy |
|---|---|
| Partition pruning | Exact |
| Table scans | Exact |
| Views, materialized views | Exact |
| Clustering | Overestimate (cluster pruning happens at execution time) |
| Query cache | Ignored (correct for regression detection) |
Structural analysis#
DiffCost parses both SQL versions into syntax trees with the sqlglot library and flags changes that usually raise cost:
- removed partition or date filters;
SELECT *added;- new
JOINs, especiallyCROSS JOINs; - removed
WHEREconditions; - new table references.
The flags appear in the comment's root cause analysis, next to the cost change. DiffCost computes them on the runner, from the SQL in your checkout. Reading the PR comment lists every flag.
Without BigQuery credentials#
Without credentials (for example on PRs from forks), DiffCost still runs the structural analysis, on plain SQL files and on dbt models from a manifest compiled in the run. You get a structural-only report: risk flags, but no dollar amounts. Every changed query then has an unknown cost, so on a paid plan with a threshold the PR is blocked. (In a dbt repository, a fork PR has no compiled manifest because the dbt compile step is skipped, so DiffCost reports "could not analyze" instead; see dbt projects.) (PRs from forks also get no API key, so no status is posted for them; see Merge blocking.)
If credentials were provided but can't be used (for example a malformed key), the comment says so and gives the reason, instead of "no credentials".
Queries whose cost is unknown#
A query DiffCost couldn't price has an unknown cost. Its row shows Cost unavailable with the reason, or "PR dry-run failed" or "Production dry-run failed" with BigQuery's error, and it's left out of the totals. DiffCost never treats an unknown cost as $0: when a threshold applies, it blocks the PR with the reason "N queries could not be costed". Typical reasons:
- the dry run failed, for example "Access Denied" or a timeout;
- there is no base version to compare against (a dbt model that existed before, with no baseline manifest);
- a dbt model changed in the PR, but one of the two manifests didn't compile it;
- the SQL file is a symlink in the PR (DiffCost never follows it);
- the file was a symlink on the base branch: git stores the link's target path, which isn't SQL, so the base dry run fails;
- in a dbt repository,
.sqlfiles changed outside the dbt project DiffCost analyzed.
dbt models that only fail because they depend on objects the same PR creates are marked Not costed instead; see dbt projects.
What DiffCost never does#
- It never runs your queries: every BigQuery job DiffCost submits is a dry run.
- It never sends SQL, table, dataset, column or model names, file paths or row data to the DiffCost service. See Security and data.
- It never follows symlinks in the PR, never trusts a
.diffcost.ymlor baseline manifest the PR itself edits, and never reports a failure as "no changes".
Something missing or wrong? Email support@diffcost.dev.