← All posts
Data Engineering11 min read

dbt Query Labels in BigQuery: Fix Your Cost Attribution (2026)

Your BigQuery bill spiked and you have no idea which dbt model caused it. Here is how to fix that with dbt query labels — and the diagnostic SQL to prove it.

If your BigQuery bill jumped and you cannot tell which dbt model caused it, the root problem is almost certainly missing query labels. Without them, every job in INFORMATION_SCHEMA.JOBS looks identical — same service account, no model name, no dbt context. Enabling job-label: true in your dbt_project.yml takes under five minutes and immediately makes every dbt-executed query attributable to its source model, environment, and invocation.

This is one of the most quietly damaging gaps in a dbt-on-BigQuery setup. You have cost alerts, you have billing exports, you might even have a dashboard — but none of it tells you which model is responsible for that 3 TiB scan that ran at 02:17 on Tuesday. The investigation becomes a guessing game: check the scheduler, grep through query history, cross-reference timestamps. On a project with 80 models running on a nightly job, that is a day of work every time the bill spikes.

A pattern we see repeatedly in early-stage companies — particularly fintech and payments businesses on BigQuery — is that cost governance is the last thing that gets set up and the first thing that causes a crisis. By the time the bill triples quarter-on-quarter, nobody can tell you whether it is one bad model, a new data volume event, or a BI tool doing full table scans through a service account. Without query labels, you are flying blind.

Why BigQuery Cannot Tell You Which dbt Model Ran Without Labels

INFORMATION_SCHEMA.JOBS is BigQuery's primary source of query history and cost data. BigQuery does not expose per-query cost directly in system tables — instead, cost is estimated by combining query usage with a pricing input. What it does expose is total_bytes_billed, user_email, job_id, start_time, and — critically — labels. Without labels, the labels column is either empty or populated only with generic system metadata.

When dbt runs a model, it submits a BigQuery job. If you have not configured query labels, that job arrives with no dbt-specific metadata attached. You can see that a job ran, how much it scanned, and when — but you cannot see that it was fct_payments versus dim_customers versus int_settlement_reconciled. At scale, this is not a minor inconvenience. It is a complete loss of cost observability.

dbt solves this with two mechanisms: query-comment and job-label. The query-comment feature embeds a JSON comment at the top of every query dbt submits. The job-label flag — which most teams miss — instructs dbt to parse that comment and attach it as actual BigQuery job labels, making the data queryable through INFORMATION_SCHEMA.JOBS.

Data engineer reviewing BigQuery dbt model cost attribution breakdown on dual monitors

How to Enable dbt Query Labels in BigQuery

The fix is a three-line addition to your dbt_project.yml:

# dbt_project.yml
query-comment:
  comment: "{{ {'app': 'dbt', 'dbt_model': node.name, 'dbt_invocation_id': invocation_id, 'dbt_env': target.name} | tojson }}"
  job-label: true

Breaking this down:

  • comment defines the JSON structure dbt will embed. You control exactly what context gets attached — model name, environment (target.name), invocation ID, and anything else you want to query later.
  • job-label: true is the key instruction. Without it, the comment is written into the query text but never surfaces as a queryable BigQuery label. With it, dbt extracts the key-value pairs from the comment and submits them as BigQuery job labels.

Important constraint: BigQuery job label keys and values must be lowercase strings, must begin with a letter or underscore, cannot exceed 63 characters, and the total number of labels per job is capped at 64. Keep your label values short. If dbt_model contains a model name longer than 63 characters (unusual but possible in deeply nested projects), it will be silently truncated — worth checking.

Once this is live, every dbt job in BigQuery will carry labels like:

labels.app = "dbt"
labels.dbt_model = "fct_payments"
labels.dbt_env = "production"
labels.dbt_invocation_id = "a3f2c1d9-..."

Now you have something to query against.

The Diagnostic SQL: Find Your Most Expensive dbt Models Right Now

Once labels are flowing, run this against INFORMATION_SCHEMA.JOBS in your BigQuery project to get a cost breakdown by dbt model over the last 30 days:

SELECT
  labels.value AS dbt_model,
  COUNT(*) AS job_count,
  ROUND(SUM(total_bytes_billed) / POW(1024, 4), 4) AS total_tib_billed,
  ROUND(SUM(total_bytes_billed) / POW(1024, 4) * 6.25, 2) AS estimated_cost_usd
FROM
  `region-eu.INFORMATION_SCHEMA.JOBS`,
  UNNEST(labels) AS labels
WHERE
  creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
  AND labels.key = 'dbt_model'
  AND job_type = 'QUERY'
  AND state = 'DONE'
GROUP BY
  dbt_model
ORDER BY
  total_tib_billed DESC
LIMIT 25;

A few notes on this query:

  • Replace region-eu with your actual BigQuery region (e.g. region-us, us, eu). The INFORMATION_SCHEMA.JOBS view is region-scoped.
  • The $6.25 per TiB rate is the BigQuery on-demand pricing as of mid-2026 — adjust if you are on a different tier or reserved capacity.
  • UNNEST(labels) is necessary because BigQuery stores labels as a repeated STRUCT. Each label is a key-value pair; unnesting lets you filter and pivot on individual keys.
  • If you are on slots/reservations rather than on-demand, replace the cost column with a slots-hours metric from total_slot_ms.

This query will immediately surface your top 25 most expensive dbt models. In almost every project we have audited, the top three models account for 50–70% of total dbt compute spend — and at least one of them is a surprise to the team.

If your labels column comes back empty for dbt jobs, job-label: true is not yet active. Check that you are on dbt-bigquery >= 1.0.0 and that your dbt_project.yml changes have been deployed to your production environment.


Running that query for one model is fine. Checking every model across your entire project — and getting alerted when a new model becomes a top spender — is not something you want to do manually on a schedule. fintel-scan is a free, open-source MIT CLI that automates this check and fourteen others locally with no warehouse connection required: uvx fintel-scan.


Breaking Down Cost by Environment and Invocation

Once you have model-level attribution working, the next step is slicing by environment. Many teams run dbt in production, staging, and sometimes a ci environment triggered on pull requests. Without environment labels, a CI run that accidentally scans a full production table is invisible — it just adds to the project bill with no obvious owner.

Add this variant to your diagnostic toolkit:

SELECT
  (SELECT value FROM UNNEST(labels) WHERE key = 'dbt_env') AS environment,
  (SELECT value FROM UNNEST(labels) WHERE key = 'dbt_model') AS dbt_model,
  COUNT(*) AS job_count,
  ROUND(SUM(total_bytes_billed) / POW(1024, 4) * 6.25, 2) AS estimated_cost_usd
FROM
  `region-eu.INFORMATION_SCHEMA.JOBS`
WHERE
  creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
  AND EXISTS (
    SELECT 1 FROM UNNEST(labels) WHERE key = 'dbt_env'
  )
  AND job_type = 'QUERY'
  AND state = 'DONE'
GROUP BY
  environment, dbt_model
ORDER BY
  estimated_cost_usd DESC
LIMIT 50;

This query breaks cost down by both environment and model. In one project we worked on with a Series A fintech, a CI pipeline had been running dbt build --select +fct_transactions on every PR — including the full parent chain — against the production dataset. The model itself was partitioned but the parent staging model was not, and every CI run was doing a 400 GB full scan. Nobody had noticed because the cost was attributed to the same service account as production. Once labels were in place, it showed up immediately as a ci environment cost outlier.

The fix in that case was to point the CI profile at a smaller development dataset and add a partition filter to the staging model. The change took two hours to implement and cut weekly BigQuery spend on that project by roughly 18%.

dbt query label configuration in YAML file alongside BigQuery cost attribution report

What to Do With the Attribution Data Once You Have It

Query labels are a means to an end. Once you can see cost by model and environment, the useful actions are:

Set cost thresholds per model. If fct_payments has historically cost $0.40 per run and it suddenly costs $4.00, something changed — a new data volume event, a schema change that broke a partition filter, or an upstream model that started sending more rows. You want an alert on that, not a bill at the end of the month.

Identify models that are candidates for incremental materialisation. If a model is billing 200 GB every run and it processes daily transaction data, it almost certainly should be incremental with a partition filter — not a full table rebuild. The label data will show you exactly which models to prioritise. See our post on dbt Incremental Subquery Filter in BigQuery: Full Scan Every Run for what breaks when you get this wrong.

Audit your BI tool's query patterns. BI tools that connect to BigQuery via service accounts will not have dbt labels — their queries are not dbt jobs. But once you subtract labelled dbt cost from total project cost, the remainder is attributable to direct queries, BI tools, and scheduled queries. That gap is often where the real surprises live.

Build a cost dashboard. Export the label-enriched INFORMATION_SCHEMA data to a permanent table on a daily schedule, and you have a time-series cost record by model that you can visualise in any BI tool. Cloud Billing export to BigQuery enables you to export detailed billing data automatically throughout the day, and you can then access it for detailed analysis or visualise it with a tool like Looker Studio. Combining billing export with INFORMATION_SCHEMA job labels gives you a complete picture: billing-side cost and query-side attribution in the same place.

Stopping Cost Attribution Gaps From Recurring

The operational failure that leads to missing labels is almost always the same: dbt_project.yml is updated in a feature branch, tested locally, but the change is never validated in the production profile before merge. Or the change is made in one dbt project but the organisation has multiple dbt projects sharing the same BigQuery environment — and the other projects are not updated.

For multi-project architectures, this is a real risk. If you have split your dbt project into separate domain repos — say, one for finance models and one for product models — you need to ensure the query-comment configuration is replicated and enforced in every project's dbt_project.yml. A governance check in your CI pipeline that validates the presence of job-label: true will catch this before it reaches production. Something as simple as a grep step in your GitHub Actions workflow is sufficient.

For teams on dbt Multi-Project Architecture in BigQuery, this governance step is non-negotiable — without it, you will routinely have cost attribution gaps whenever a new sub-project is spun up.

Finally, consider adding the dbt_invocation_id label. This gives you the ability to tie every model run in a single dbt run invocation together, which is invaluable when debugging a run that produced unexpected results. You can pull every job from that invocation — across all models — and reconstruct exactly what happened in sequence.

Frequently Asked Questions

Q: What is job-label: true in dbt and why does it matter for BigQuery cost?

A: job-label: true is a configuration flag in dbt_project.yml that instructs dbt to extract key-value pairs from the query-comment block and submit them as BigQuery job labels. Without it, dbt's query context is embedded as a SQL comment but is not queryable through INFORMATION_SCHEMA.JOBS. With it, you can attribute every byte billed in BigQuery to a specific dbt model, environment, and invocation.

Q: How do I query BigQuery INFORMATION_SCHEMA to find cost by dbt model?

A: Query INFORMATION_SCHEMA.JOBS in your BigQuery region, unnest the labels repeated field, filter on labels.key = 'dbt_model', and sum total_bytes_billed. Multiply TiB by your on-demand rate ($6.25 per TiB at mid-2026 pricing) to get estimated cost. This requires job-label: true to be active in your dbt configuration first.

Q: Will enabling dbt query labels affect my BigQuery query performance?

A: No. Job labels are metadata attached to the BigQuery job submission — they have no effect on query execution, scan volume, or slot consumption. The overhead of adding labels is negligible and entirely in BigQuery's job API layer, not in query processing.

Q: How many labels can a BigQuery job have, and are there any restrictions?

A: BigQuery supports a maximum of 64 labels per job. Label keys and values must be lowercase strings, must start with a letter or underscore, and cannot exceed 63 characters. If your dbt model name exceeds 63 characters it will be silently truncated, so keep model names concise and validate after enabling labels.

Q: Can I use dbt query labels to attribute cost to specific teams or domains?

A: Yes — this is one of the most useful extensions. Add a dbt_domain or dbt_team key to your query-comment block and populate it using a dbt variable or a custom macro that reads from your model's meta config block. You can then slice INFORMATION_SCHEMA.JOBS cost data by team, producing a chargeback or showback report per domain.

If you are running a growing dbt project on BigQuery and your cost attribution is still a manual investigation every time a bill spikes, that is a fixable problem — and the fix is a five-line change to a config file. At Fintel Analytics, we have helped fintech, payments, and e-commerce businesses build proper cost governance into their dbt projects from the ground up — from enabling labels and building attribution dashboards through to setting alerting thresholds by model and environment. The same work that makes your bill explainable also makes your pipeline defensible to a finance team or an investor asking where the infrastructure spend is going.

← Back to all posts