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:
dbt source freshnessThe 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: invoicesIndividual 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.
# A typical scheduled job sequencedbt source freshnessdbt rundbt testSome 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_pipelinetask run_models: depends_on: freshness_check command: dbt runFreshness 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.
dbt source freshnesscat 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
| Element | Purpose |
|---|---|
loaded_at_field | The column dbt reads to determine data recency |
warn_after / error_after | Thresholds defining acceptable staleness |
dbt source freshness | The command that runs the check |
target/sources.json | Machine-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.