Data Engineering  /  dbt

๐Ÿ”„ dbt โ€” Data Build Tool 60 guides ยท updated 2026

Analytics engineering with SQL โ€” models, tests, sources, and Jinja macros that turn raw warehouse tables into trustworthy, documented data products.

dbt Source Freshness: Catching Stale Data Before Your Users Do

A dashboard showing yesterdayโ€™s numbers as if they were todayโ€™s is one of the most damaging failures a data team can ship, precisely because nothing looks broken. The query runs fine, the numbers are plausible, and the only thing wrong is that the underlying source stopped updating six hours ago and nobody noticed. dbtโ€™s source freshness feature exists specifically to catch this class of silent failure before a stakeholder does.


What Freshness Actually Checks

Freshness doesnโ€™t inspect your transformed data โ€” it checks the raw source table directly, looking at a timestamp column to determine how long ago the most recent row was loaded. If that gap exceeds a threshold you define, dbt reports a warning or an error, without needing to touch a single downstream model.

sources:
- name: raw
schema: raw_app_data
tables:
- name: orders
loaded_at_field: _fivetran_synced
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}

Here, loaded_at_field tells dbt which column represents โ€œwhen this row arrived.โ€ Most ELT tools (Fivetran, Airbyte, Stitch) add a metadata column exactly for this purpose โ€” _fivetran_synced, _airbyte_extracted_at, and similar.


Running a Freshness Check

Freshness is checked with its own dedicated command, separate from dbt run or dbt test:

Terminal window
dbt source freshness

The output tells you, per source table, whether itโ€™s fresh, stale-but-warning, or stale-and-erroring:

1 of 3 START freshness of raw.orders ................ [RUN]
1 of 3 PASS freshness of raw.orders .................. [PASS in 0.84s]
2 of 3 START freshness of raw.customers .............. [RUN]
2 of 3 WARN freshness of raw.customers ............... [WARN in 0.71s]
3 of 3 START freshness of raw.inventory .............. [RUN]
3 of 3 ERROR freshness of raw.inventory ............... [ERROR in 0.68s]

A WARN means the data is older than your warn_after threshold but not yet at error_after โ€” worth a look, not yet a crisis. An ERROR means the source has exceeded your maximum acceptable staleness and something upstream almost certainly needs attention.


Setting Sensible Thresholds

Thresholds should reflect how the source actually updates, not an arbitrary round number. A source syncing every 15 minutes and one syncing once nightly need very different thresholds.

tables:
- name: orders # syncs every 15 min via webhook
loaded_at_field: _synced_at
freshness:
warn_after: {count: 1, period: hour}
error_after: {count: 3, period: hour}
- name: finance_export # nightly batch load
loaded_at_field: _synced_at
freshness:
warn_after: {count: 30, period: hour}
error_after: {count: 48, period: hour}

Setting a one-hour threshold on a table that only updates nightly guarantees a constant stream of false-positive warnings that the team learns to ignore โ€” which defeats the entire point. Calibrate thresholds to the sourceโ€™s actual update cadence, not a blanket policy.


Freshness at the Source-Block Level

If every table in a source group shares the same update cadence, you can set freshness once at the source level instead of repeating it per table.

sources:
- name: stripe
schema: stripe_billing
loaded_at_field: _fivetran_synced
freshness:
warn_after: {count: 6, period: hour}
error_after: {count: 12, period: hour}
tables:
- name: charges
- name: subscriptions
- name: invoices

Individual tables can still override this if one of them genuinely has a different SLA โ€” table-level config always takes precedence over source-level defaults.


Wiring Freshness Into Orchestration

Running dbt source freshness manually catches nothing on its own โ€” it needs to run on a schedule, before your regular dbt run, so a stale source is caught before you build downstream models on top of it.

Terminal window
# A typical scheduled job sequence
dbt source freshness
dbt run
dbt test

Some teams take this further and make a failed freshness check block the rest of the pipeline entirely, since building fresh transformations on top of stale raw data just produces confidently-wrong output faster.

# Example: a simple orchestration step (pseudocode for Airflow/Prefect)
task freshness_check:
command: dbt source freshness
on_failure: stop_pipeline
task run_models:
depends_on: freshness_check
command: dbt run

Freshness Results as Machine-Readable Output

Every freshness run produces a sources.json artifact in the target/ directory, containing the exact freshness status of every checked table. This is what makes freshness genuinely useful in production rather than just a manual command someone remembers to run occasionally โ€” the JSON output can feed alerting systems directly.

Terminal window
dbt source freshness
cat target/sources.json | jq '.results[] | select(.status != "pass")'

This pattern โ€” piping freshness results into an alerting step that pages someone only on actual staleness, not on every run โ€” is the difference between freshness checks that get ignored and ones that actually catch incidents.


Common Mistakes

Forgetting loaded_at_field entirely. Without it, dbt has no way to compute freshness and the check silently canโ€™t run for that table โ€” always double-check the field actually exists and is populated by your loader.

Using an application timestamp instead of a load timestamp. created_at on the source row tells you when the event happened, not when it arrived in your warehouse. Freshness checks need the load timestamp specifically, or youโ€™ll get misleading staleness readings for late-arriving data.

Setting thresholds nobody reviews. A freshness check that alerts into a channel nobody watches is functionally the same as having no check at all. Route failures somewhere theyโ€™ll actually be seen and acted on.

Summary

ElementPurpose
loaded_at_fieldThe column dbt reads to determine data recency
warn_after / error_afterThresholds defining acceptable staleness
dbt source freshnessThe command that runs the check
target/sources.jsonMachine-readable output for alerting integration

Source freshness is one of the cheapest, highest-leverage checks you can add to a dbt project. It takes minutes to configure and catches an entire category of failure โ€” silent staleness โ€” thatโ€™s otherwise invisible until someone downstream notices the numbers look wrong.