In dbt, how does the source freshness check decide a raw table is stale?
answer
- the newest row, versus now
- one small query per source table
- a column plus two thresholds
- max of loaded_at_field against warn and error
basics
~20 sIt takes the maximum value of the source table's loaded_at_field, measures the lag to the current time, and compares that lag with the warn_after and error_after thresholds in the source YAML, returning pass, warn or error per table.
solid answer
~50 s`dbt source freshness` runs one query per configured source table — essentially `select max(loaded_at_field)` plus the warehouse's current timestamp — and computes how far behind that maximum is. The thresholds live under a `freshness:` block as `warn_after` and `error_after`, each expressed as `{count: N, period: minute|hour|day}`, and can be set on the source and overridden per table. The result per table is pass, warn or error, printed to the log and written to a `sources.json` artifact. Two configuration details matter in practice: `loaded_at_field` should be an *ingestion* timestamp, not a business event time, or you are measuring how recent the data is rather than whether the pipeline ran; and a `filter` on the freshness block adds a `where` clause so the `max()` does not scan a multi-terabyte partitioned table. Freshness is its own command — a normal run does not evaluate it.
code
yaml · 15 linesversion: 2
sources:
- name: jaffle_shop
database: raw
schema: jaffle_shop
loaded_at_field: _etl_loaded_at
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}
filter: _etl_loaded_at >= dateadd(day, -3, current_date)
tables:
- name: orders
- name: country_lookup
freshness: null # static table, never stalego deeper
Recall that dbt can check whether a raw table has been loaded recently, that it needs a timestamp column named in YAML, and that it is a separate command rather than part of a normal run.
Explain the mechanics: the max of loaded_at_field versus now, graded against warn_after and error_after in count/period form, producing pass, warn or error per table plus an artifact.
Demonstrate judgment about the timestamp column and the thresholds — ingestion time not event time, filters to keep the check cheap, and thresholds set where a human should actually act rather than at the pipeline's best case.
Own freshness as an SLA instrument: which sources carry a published promise, who is paged when one breaks, and how thresholds are agreed with the producing teams rather than guessed by the analytics team.
## What the command actually does `dbt source freshness` is a standalone command. For every source table that has freshness configured, dbt issues a small query against the warehouse of roughly this shape: ```sql select max(_etl_loaded_at) as max_loaded_at, current_timestamp as snapshotted_at from raw.jaffle_shop.orders ``` It then computes the lag between `max_loaded_at` and `snapshotted_at` and grades it against the thresholds you declared. Nothing is transformed and nothing is written to your models; the only output is a verdict per table plus an artifact. ## The configuration ```yaml sources: - name: jaffle_shop database: raw schema: jaffle_shop freshness: # source-level default warn_after: {count: 12, period: hour} error_after: {count: 24, period: hour} loaded_at_field: _etl_loaded_at tables: - name: orders - name: pricing_reference freshness: null # opt this table out ``` `period` accepts `minute`, `hour` or `day`. Both `freshness` and `loaded_at_field` can be set once on the source and overridden per table, and setting `freshness: null` on a table opts it out — useful for genuinely static reference tables where a stale warning is noise. ## Choosing loaded_at_field This is the decision that determines whether the check means anything. The column should record **when the row landed in the warehouse**, not when the business event happened. Ingestion tools usually provide one (`_etl_loaded_at`, `_fivetran_synced`, `_airbyte_extracted_at`), or your load job can stamp one. If you point `loaded_at_field` at an event timestamp such as `order_placed_at`, the check reports on customer behaviour rather than on your pipeline: a quiet Sunday night with no orders produces a stale alert even though ingestion is perfectly healthy, and a pipeline that has been dead for a day can still look fresh if it happens to have loaded a batch containing future-dated events. Two further gotchas. The column must be castable to a timestamp; dbt compares in UTC, so a naive local-time column produces a constant offset that silently shifts every threshold. And the field must be indexed or partitioned in a way that makes `max()` cheap, which leads directly to the next point. ## The filter key On a large partitioned table, `select max(loaded_at_field)` can be a full scan and a real bill. The `filter` key injects a `where` clause into the freshness query: ```yaml freshness: warn_after: {count: 1, period: hour} filter: _etl_loaded_at >= dateadd(day, -3, current_date) ``` Now the query only touches the last three days of partitions. The tradeoff is that if the table goes stale for longer than the filter window, `max()` returns null and the check reports an error — which for freshness purposes is the correct outcome anyway. ## Metadata-based freshness Newer dbt versions can, on some adapters, compute freshness from warehouse metadata (a table's last-modified time in the information schema) with no `loaded_at_field` at all. That avoids scanning the table entirely, but it measures when the *table object* was last written, which is not the same as when the newest row arrived — a full-refresh rewrite of old data will look fresh. Treat it as a cheap approximation and keep an explicit `loaded_at_field` where the distinction matters. Check what your adapter and dbt version support before relying on it. ## Outputs Each table gets one of three states. `pass` means the lag is under both thresholds. `warn` means it exceeded `warn_after` but not `error_after`. `error` means it exceeded `error_after`. Results are printed and written to a `sources.json` artifact in the target directory, which is what tooling reads to render freshness dashboards, and what state-comparison selection uses to find sources that received new data since the last run. ## Selecting what to check The usual selectors apply: `dbt source freshness --select source:jaffle_shop` checks one source, `source:jaffle_shop.orders` one table. Teams typically run the whole check on a schedule that matches their tightest SLA rather than on every deploy. ## Setting thresholds that are worth having Thresholds should encode the promise you made to consumers, not the pipeline's best case. If the hourly load usually finishes in eight minutes, `warn_after: 1 hour` fires on every hiccup and gets muted within a week. Set `warn_after` where a human should look and `error_after` where a downstream consumer would be misled, and expect the two to be hours apart for a daily pipeline.
- Why is an event timestamp a poor choice for loaded_at_field?Because it measures the business, not the pipeline. A quiet weekend with no new events makes a healthy pipeline look stale, and a batch containing future-dated events can make a dead pipeline look fresh. Use a column stamped when the row landed — the loader's sync timestamp or one your load job writes — so the check reports on ingestion.
- What does the filter key on a freshness block change about the query dbt runs?It adds a `where` clause to the freshness query, so the `max()` only scans recent partitions instead of the whole table — important on multi-terabyte tables where the check itself would otherwise be expensive. If staleness ever exceeds the filter window the max comes back null and the check errors, which is the right verdict anyway.
- How do you exclude a genuinely static reference table from freshness checks?Set `freshness: null` on that table to override the source-level default. Static lookups have no meaningful load timestamp, and leaving them in produces permanent warnings that train people to ignore the whole report. Alternatively, do not set `loaded_at_field` for them at all.
saying these in an interview costs you the question
- Thinks dbt run or dbt build evaluates freshness thresholds
- Points loaded_at_field at a business event timestamp
- Says the check compares row counts between runs
- Believes freshness scans the whole table unavoidably
- Cannot name warn_after and error_after or their count/period shape