MetricSign
Start free
Data Observability7 min·

dbt Source Freshness: How to Monitor It and Alert on Violations

Learn how to configure dbt source freshness, understand its limits, and link violations to downstream Power BI and Table

What Is dbt Source Freshness?

dbt source freshness is a built-in mechanism that checks whether the raw data in your source tables is recent enough for your pipelines to be trusted. It works by querying a loaded_at_field — typically a timestamp column like _airbyte_emitted_at or updated_at — and comparing the most recent value against configurable warn and error thresholds.

If the latest row in your source table is older than the threshold you defined, dbt marks that source as warn or error. This check runs as part of dbt source freshness (or dbt build with freshness enabled) and surfaces in your dbt Cloud job logs or CLI output.

Here is what a typical freshness result looks like in the CLI:

Found 4 sources. Freshness of 4 sources will be determined.

Source 'jaffle_shop.orders'
  warn after: 12 hours
  error after: 24 hours
  Max loaded_at: 2026-06-07 08:33:00 UTC
  Age: 26h 14m
  Status: error

The distinction between warn and error matters for pipeline control: an error status causes dbt source freshness to exit with a non-zero code, which can block downstream models from running if you wire your orchestrator accordingly. A warn is advisory — it shows up in the output but does not fail the job by default.

Freshness checks are evaluated at the source level, not at the model level. That means if three models all depend on jaffle_shop.orders, a single stale source can silently propagate through all three before anyone notices.

Configuring Freshness in schema.yml

Freshness is declared in your schema.yml (or any sources: YAML file in your dbt project). You define thresholds at either the source level (applies to all tables in that source) or at the individual table level.

version: 2

sources:
  - name: jaffle_shop
    database: raw
    schema: public
    freshness:
      warn_after:
        count: 12
        period: hour
      error_after:
        count: 24
        period: hour
    loaded_at_field: _etl_loaded_at

    tables:
      - name: orders
        description: Raw orders from the transactional database.

      - name: customers
        description: Customer master data.
        freshness:
          warn_after:
            count: 6
            period: hour
          error_after:
            count: 12
            period: hour
        loaded_at_field: updated_at

      - name: events
        freshness: null  # skip freshness check for this table

A few things worth noting:

  • loaded_at_field must be a timestamp column that reliably reflects when data was loaded or updated. Using a business timestamp like order_date is usually wrong — it reflects event time, not ingestion time.
  • Setting freshness: null on a table disables the check entirely for that table, even if the parent source has a default configured.
  • period accepts minute, hour, or day.
  • If loaded_at_field is missing or the column contains NULLs, dbt will raise a compilation or runtime error.

Once configured, run the check with:

dbt source freshness

Or target a specific source:

dbt source freshness --select source:jaffle_shop.orders

The results are also written to target/sources.json, which downstream tooling (including MetricSign) can consume to correlate freshness state with downstream impact.

In dbt Cloud, freshness checks appear as a separate step in job logs. You can configure the job to continue on warn but fail on error, giving you a clear signal when data is genuinely late.

Downstream Impact: Stale Data in Power BI and Tableau

A freshness violation in dbt is not just a dbt problem. The real damage happens downstream — in the BI layer where stakeholders are making decisions.

Here is a typical failure chain:

  1. Your Airbyte pipeline from Salesforce misses a sync window due to an API rate limit.
  2. The salesforce.opportunities source table stops receiving new rows.
  3. dbt runs on schedule and flags salesforce.opportunities as stale (error after 6 hours).
  4. If your orchestrator is not wired to block downstream models on freshness errors, fct_pipeline_revenue refreshes anyway — from stale data.
  5. The Power BI dataset backed by fct_pipeline_revenue refreshes successfully. No error. No alert.
  6. Your sales director opens the revenue dashboard and sees numbers that are 8 hours old — without any indication that the data is stale.

This is the most dangerous failure mode: silent staleness. The BI layer reports a successful refresh because, from its perspective, the data warehouse query succeeded. It has no way to know that the upstream source was stale when dbt ran.

In Tableau, the same pattern occurs with published data sources connected to Snowflake or BigQuery views. The extract refresh completes with a green checkmark, but the underlying view materializes from stale dbt models.

For ADF pipelines that trigger Power BI dataset refreshes via the Power BI REST API, there is no built-in mechanism to check dbt freshness state before triggering the refresh. The pipeline runs, the dataset refreshes, and the staleness is invisible.

The gap: dbt knows the data is stale. Power BI does not. There is no native bridge between them.

Quantifying the risk: if your fct_pipeline_revenue model runs every hour and your Salesforce sync is delayed by 4 hours, you will serve stale data to 4 consecutive dashboard refreshes before anyone is alerted — assuming someone is watching dbt logs at all.

Limits of dbt's Native Freshness Checks

dbt source freshness is a detection mechanism, not an alerting or impact-mapping system. Understanding what it does and does not do helps you avoid building false confidence into your data quality layer.

What dbt freshness does: - Queries the loaded_at_field and computes staleness against your thresholds - Emits warn or error status in CLI output and target/sources.json - Exits non-zero on error, enabling orchestrators to block downstream runs - Surfaces results in dbt Cloud job logs

What dbt freshness does not do: - Alert your team via Slack, PagerDuty, or email without additional tooling - Map which downstream dbt models depend on the stale source - Know which Power BI datasets, Tableau workbooks, or ADF pipelines will be affected - Track historical freshness trends over time (only the latest run is visible) - Distinguish between a source that is 1 hour stale versus 8 hours stale in its downstream impact

For example, target/sources.json contains a snapshot of the last freshness run:

{
  "metadata": {"dbt_schema_version": "..."},
  "results": [
    {
      "unique_id": "source.jaffle_shop.jaffle_shop.orders",
      "status": "error",
      "timing": [...],
      "thread_id": "Thread-1",
      "execution_time": 0.84,
      "max_loaded_at": "2026-06-07T08:33:00+00:00",
      "snapshotted_at": "2026-06-08T10:47:00+00:00",
      "age_in_seconds": 94440.0
    }
  ]
}

This file tells you the source is stale. It does not tell you that three Power BI Premium datasets refreshed from that stale source in the meantime, that your CFO's financial dashboard is now showing yesterday's numbers, or that the ADF pipeline that triggered those refreshes completed without errors.

To close that gap, you need something that reads dbt freshness state and correlates it with downstream dataset refresh activity in real time.

Linking dbt Freshness to Downstream Staleness

MetricSign connects dbt source freshness violations directly to the downstream BI and pipeline events that were affected. Instead of checking dbt logs in one tab and Power BI refresh history in another, you get a single incident timeline.

How it works:

MetricSign ingests target/sources.json after each dbt run (via API push or polling) and cross-references it with the refresh activity it collects from the Power BI REST API, Tableau Server/Cloud REST API, and ADF run history. When a source enters error or warn state, MetricSign traces the lineage forward to identify which datasets were refreshed during or after that freshness window.

For Power BI, the relevant API call MetricSign uses is:

GET https://api.powerbi.com/v1.0/myorg/groups/{groupId}/datasets/{datasetId}/refreshes

This returns a refresh history with startTime, endTime, and status. MetricSign compares the startTime of each refresh against the max_loaded_at from dbt's freshness output. If a dataset refresh started after the source went stale and before the source recovered, that refresh is flagged as a stale refresh in the incident log.

In the MetricSign incident view, a dbt freshness violation surfaces as:

[INCIDENT] Source freshness error — jaffle_shop.orders
Detected: 2026-06-08 10:47 UTC
Stale since: 2026-06-07 08:33 UTC (26h 14m)

Affected downstream refreshes:
  - Power BI: Sales Pipeline Report [workspace: Revenue]  ← refreshed at 10:00 UTC (stale)
  - Power BI: CFO Dashboard [workspace: Finance]          ← refreshed at 09:00 UTC (stale)
  - ADF Pipeline: trigger_pbi_refresh                    ← completed at 10:02 UTC

Alert routing is configurable per source. For salesforce.opportunities, you might route error alerts to the #data-reliability Slack channel and create a PagerDuty incident if staleness exceeds 12 hours.

For Fabric and KQL users, MetricSign also supports querying refresh history via the Fabric REST API and correlating it with dbt freshness state in the same incident timeline. If you use Fabric eventhouse and want to query freshness violations in KQL:

FreshnessViolations
| where source_name == "jaffle_shop.orders"
| where status == "error"
| join kind=inner (
    DatasetRefreshes
    | where refresh_start > ago(48h)
) on $left.snapshotted_at >= $right.refresh_start
| project source_name, max_loaded_at, snapshotted_at, dataset_name, refresh_start, refresh_status
| order by refresh_start desc

This query returns every dataset refresh that ran while the source was in a freshness error state — giving you the exact blast radius of a stale source without manually correlating timestamps across systems.

Practical Setup: End-to-End Freshness Monitoring

Here is a concrete setup that covers the full loop from dbt configuration to downstream alerting.

Step 1: Configure freshness in dbt

Add thresholds to your highest-risk sources first — typically transactional data from Salesforce, Hubspot, or Stripe where latency has business impact:

version: 2

sources:
  - name: salesforce
    database: raw
    schema: salesforce
    loaded_at_field: _airbyte_emitted_at
    freshness:
      warn_after: {count: 4, period: hour}
      error_after: {count: 8, period: hour}
    tables:
      - name: opportunities
      - name: accounts
      - name: contacts
        freshness:
          warn_after: {count: 12, period: hour}
          error_after: {count: 24, period: hour}

Step 2: Run freshness as part of your CI/CD or orchestration

In Airflow or Prefect, add a freshness check task before your dbt model runs:

# Fail fast if sources are stale
dbt source freshness --select source:salesforce

# Only proceed if freshness passes
dbt run --select tag:daily

In dbt Cloud, enable the "Run source freshness" option in your job settings and configure the job to "Error on source freshness error" to block downstream model execution.

Step 3: Push freshness results to MetricSign

After each dbt run, post target/sources.json to the MetricSign ingest API:

curl -X POST https://app.metricsign.com/api/dbt/freshness \
  -H "Authorization: Bearer $METRICSIGN_API_KEY" \
  -H "Content-Type: application/json" \
  -d @target/sources.json

Or use the MetricSign dbt package (adds a post-hook that handles this automatically):

# packages.yml
packages:
  - package: metricsign/dbt_metricsign
    version: [">=0.3.0", "<0.4.0"]

Step 4: Configure downstream dataset mapping

In MetricSign, map each dbt source to the Power BI workspaces and datasets that depend on it. This is a one-time setup that enables automatic blast-radius detection when freshness violations occur.

Step 5: Set alert thresholds

Configure separate alert channels for warn (Slack notification) and error (PagerDuty + Slack) so your team is not woken up for advisory warnings but is immediately paged for confirmed staleness that has affected downstream consumers.

With this setup, the time between a freshness violation occurring and your team knowing which dashboards are affected drops from hours (if anyone notices at all) to under 5 minutes.

Frequently asked questions

What is the difference between dbt source freshness warn and error?+
A `warn` status means the source data is older than your warning threshold but has not yet crossed the error threshold. dbt logs the warning but the `dbt source freshness` command exits with code 0, so jobs continue by default. An `error` status means the source has crossed your error threshold — dbt exits with a non-zero code, which can be used to block downstream model runs in your orchestrator. In practice, set warn thresholds to give your team early notice and error thresholds to represent the point where downstream data quality is genuinely compromised.
Does dbt source freshness work with all data warehouses?+
Yes, dbt source freshness works with any warehouse that dbt supports: Snowflake, BigQuery, Databricks, Redshift, DuckDB, and others. The check is a simple SQL query against the `loaded_at_field` column you specify, so it runs wherever dbt can connect. The only requirement is that the column you use as `loaded_at_field` must be a timestamp type that the warehouse can compare with `CURRENT_TIMESTAMP`. Some warehouse-specific timezone handling differences may affect results if your `loaded_at_field` stores timestamps in local time rather than UTC.
How often should I run dbt source freshness checks?+
Run freshness checks on the same schedule as your data ingestion — or more frequently. If your Airbyte syncs run every hour, run freshness checks every 30-60 minutes so you catch failures before your dbt models run. For daily batch pipelines, running freshness once before the morning dbt job is usually sufficient. The key is that freshness checks should run early enough in your pipeline to block downstream model execution if a source is stale, not after models have already materialized from stale data.
Can I configure dbt source freshness per table rather than per source?+
Yes. You can set a default `freshness` and `loaded_at_field` at the source level and override both at the individual table level. Tables with higher business criticality — like live transaction data — can have tighter thresholds (warn after 2 hours, error after 4 hours) while reference tables updated weekly can use looser thresholds. You can also set `freshness: null` on specific tables to skip the check entirely, which is useful for tables that are intentionally infrequently updated, like static lookup tables.
Why does my Power BI dataset show a successful refresh even when dbt freshness fails?+
Power BI has no visibility into dbt's freshness state. When a Power BI dataset refresh runs, it queries the data warehouse directly. If your dbt models have already materialized — even from stale source data — the query succeeds and Power BI reports a successful refresh. There is no native integration between dbt's `target/sources.json` output and the Power BI refresh API. This is why tooling like MetricSign exists: it reads both signals and flags dataset refreshes that occurred while a source was in a freshness error state, making silent staleness visible.
How do I see which downstream models are affected by a freshness violation?+
In dbt itself, you can run `dbt ls --select source:your_source+` to see all models that depend on a given source. This gives you the dbt-layer lineage. However, it does not show you which Power BI datasets, Tableau workbooks, or ADF pipelines are built on top of those models. For full end-to-end visibility — from stale source through dbt model to BI dataset — you need a tool that maps dbt lineage to downstream BI refresh activity. MetricSign maintains this mapping and surfaces it automatically when a freshness violation is detected.

Related integrations