DBT SOURCE FRESHNESS
dbt Source Freshness: dbt Freshness Checks, Config, Tests and Alerts for Stale Sources
Freshness, volume, schema and distribution monitoring across every table your dbt project reads and writes, with baselines learned per table instead of thresholds typed per source. Read only connection, alerts in Slack and PagerDuty, from 99 dollars a month.
14-day trial, no credit card, read-only connection
Alerted #data-eng 0.8s ago.
Downstream impact · consumers at risk
How do you check source freshness in dbt?
dbt checks source freshness with the dbt source freshness command, which queries the maximum value of a loaded_at_field on each source table and compares it against warn_after and error_after thresholds you declare in YAML. If neither threshold is provided, dbt will not calculate freshness for the tables in that source at all, so the check disappears rather than fails. dbt build does not run source freshness, so it happens only when you invoke the command separately, and the result is written to target/sources.json rather than delivered to anyone.
Last updated August 2026
Side by side
dbt source freshness compared
Swipe to see all columns →
| Way to check freshness in dbt | What it tells you | What you build and operate | Documented limits (dbt docs, August 2026) |
|---|---|---|---|
| dbt source freshness with warn_after and error_after | Whether the newest row in a source table is older than a window you declared, expressed as a warn or an error per table | The YAML per source, a scheduled job that runs the command, a reader for the results, deduplication, ownership and routing | If neither warn_after nor error_after is provided, dbt will not calculate freshness for the tables in that source. Thresholds are fixed counts and periods, so they cannot express that a table loads on weekdays only |
| loaded_at_field | The column dbt reads to decide when data last arrived, using max of that column against the current timestamp | Choosing and maintaining a trustworthy load timestamp on every source table | Required to calculate freshness except where dbt can use warehouse metadata. A column that records business time rather than load time will report fresh while loading is broken |
| Warehouse metadata freshness, with no loaded_at_field | When the table object last changed, taken from catalog metadata such as LAST_ALTERED on Snowflake | Nothing extra to configure, which is the appeal | Metadata moves on operations that are not loads, including backfills, column additions and merges that matched no rows, so it fails green. On the Fusion engine views are treated as always fresh because freshness cannot be determined from view metadata |
| loaded_at_query | A freshness signal you define in SQL, for streaming sources or late arriving records where a simple max is wrong | The query itself, per source, plus the reasoning about what late arrival means for each pipeline | It is your SQL, so it is your correctness problem and your cost. Every source that needs one is a source the simple path could not describe |
| filter on the freshness block | The same freshness check restricted to a recent partition so the query does not scan the whole table | A filter expression per source, kept in step with the partitioning scheme | Needed on databases where an unfiltered max would scan the full table. A filter that drifts out of step with partitioning silently narrows what the check can see |
| dbt Cloud job notifications | That a job run finished, succeeded, failed or was cancelled | The mapping from job outcome to the specific table that went stale, and from there to a person | Triggers are job run outcomes rather than model or test events, and notifications are configured per recipient rather than per job. Slack delivery covers public channels only |
| Dataobservability | Freshness, volume, schema and distribution across every table in the warehouse, scored against a baseline learned per table from its own history | A read only role, and a decision about which tables page someone | Covers Snowflake, BigQuery, Databricks and Redshift, including tables your dbt project never declares as a source. Published pricing from 99 dollars a month, 14 day trial, no card |
Positioning and pricing models are summarized in good faith from each vendor's public pages, August 2026. Verify current terms with the vendor.
What you get
What a monitoring layer adds on top of dbt source freshness
Coverage that does not stop at the sources you declared
dbt source freshness watches sources, and a source is a thing somebody wrote into a YAML file. That is a deliberate design and it works well for the tables a team remembered to declare. The gap is everything else: the staging model that a downstream dashboard depends on, the table an analyst built last quarter, the reverse ETL target, and the source that a new pipeline started writing on Tuesday and nobody added to sources.yml. Freshness derived from the warehouse inventory rather than from a declaration list covers those on the day they appear, because the input is the table list rather than the file.
Thresholds that know Tuesday is not Sunday
warn_after and error_after take a count and a period, which produces a fixed window. Most real tables do not load on a fixed window. A table loaded hourly on business days is not stale at 07:00 on Saturday, and a table that loads at 02:00 on the first of the month is not stale on the third. Fixed windows cannot encode that, so teams widen them until they stop catching real incidents, or keep them tight and train everyone to ignore the warnings. A baseline learned from each table observed load history gets the weekly and monthly shape for free, which is the difference between an alert people act on and an alert people filter.
A result that reaches a person instead of a JSON file
dbt source freshness writes its output to target/sources.json. That is the correct behavior for a command line tool and it is also where most freshness programs quietly stop, because the path from that file to the engineer carrying the pager is yours to build. In dbt Cloud the available trigger is the outcome of a job run, not the staleness of a particular source, and notifications are configured per recipient rather than per job. So a job that ran fine but flagged three stale sources looks like a success, and a job that failed for an unrelated reason pages everyone. Routing per table, per owner, with deduplication, is the missing layer.
Freshness read against the load pattern, not one timestamp
Both cheap versions of freshness are a single timestamp: the max of a column you nominated, or the moment the catalog says the object last changed. Both are honest measurements and both can be badly misleading. A loaded_at_field that carries business time reports fresh when loading has stopped. Warehouse metadata moves on a backfill, a permissions change, a no op merge or a compaction, so the table reads fresh while the dashboard goes stale. Comparing what happened against what usually happens is what separates a check that catches a broken pipeline from a check that supplies false confidence.
Freshness, volume, schema and distribution on one baseline
Freshness answers whether something arrived. It does not answer whether the right amount arrived, whether the columns still mean what they meant last week, or whether the values moved. The incidents that cost the most are usually combinations: the source landed on time, at eleven percent of normal volume, because an upstream filter changed. dbt source freshness would report green on that all day, and correctly so, because it was asked a narrower question. Watching four pillars against one baseline in one incident timeline is what turns several green checks into one honest red one.
How it works
From connected to caught
Connect the warehouse with a read only role
Create a role with read access to the databases your dbt project reads and writes. Dataobservability reads table metadata and query history to build the inventory and the freshness and volume picture, so table contents are not copied out of the warehouse. There is no agent, no change to your dbt project, and no new step in your job. Snowflake, BigQuery, Databricks and Redshift are all supported, so the coverage is the warehouse rather than the subset declared as sources.
Keep the dbt freshness checks you already trust
This is additive. If sources.yml already declares warn_after and error_after on the tables that matter most, leave them there. Declared thresholds are the right tool for a contractual expectation, such as a vendor extract that must land by 06:00 regardless of what it usually does. The monitoring layer covers the far larger set of tables nobody will ever write YAML for, and catches the volume, schema and distribution problems that a freshness window is not designed to see.
Let a baseline form before you tune anything
Give it a few days of history per table so the load pattern becomes visible, including the weekly and monthly shapes. Tuning thresholds before a baseline exists is how teams manufacture alert fatigue in week one and stop trusting the monitoring in week two. On day one the correct amount of configuration is close to none, which is the opposite of how a rules first approach starts.
Route by ownership, then let the noise settle
Send the tables that page someone to PagerDuty and everything else to Slack, and assign owners per schema so an alert arrives with a name attached. Review the first two weeks and mute the patterns that were true but not actionable. That review is the step most teams skip and then blame the tool for, and it is the difference between monitoring that survives a quarter and monitoring that gets muted in a month.
The configuration that turns the check off without telling you
The most expensive detail in dbt source freshness is a sentence in the documentation that reads like housekeeping: in the freshness block, one or both of warn_after and error_after can be provided, and if neither is provided, then dbt will not calculate freshness for the tables in this source. Read that against how configuration actually evolves. Somebody adds a freshness block during onboarding, sets warn_after, and the check works. Months later a refactor moves the block, a hierarchy change overrides it at the source level, or a table inherits an empty block, and the thresholds are gone. dbt does not fail. It does not warn. It simply stops calculating freshness for those tables, and the job stays green forever. The same shape appears in the explicit off switch, where setting freshness to null on a table disables checking for it, which is a useful feature and an easy thing to leave behind after an incident. The result is that the number of tables actually covered by dbt freshness is a number almost nobody on the team can state, and the only way to know it is to read every source file and reason about the hierarchy. Monitoring that derives its coverage from the warehouse inventory has the opposite property: coverage is the default and exclusion is the thing you have to write down.
Why dbt build does not run freshness, and what that costs in practice
dbt build runs models, tests, snapshots and seeds in dependency order, and it is the command most teams standardize on. Source freshness is not part of it. Checking freshness requires invoking dbt source freshness separately, and the intended pattern is to run it first and then use the result to select downstream work, for example with dbt build and the source_status selector to rebuild only what sits below sources that actually got fresher. That is a good pattern and it is also a second scheduled step, with its own failure modes. If the freshness step is skipped, the build still runs happily on stale data and every model downstream is rebuilt from yesterday, which is worse than not running at all because the timestamps on the outputs are new. If the freshness step fails for an infrastructure reason, you have to decide whether the build should proceed, and both answers are wrong in some situation. And because the artifact is target/sources.json, the history of what was stale and when lives only as long as your artifact retention. There is no queryable record of how often a given source has been late over the last quarter, which is precisely the question you need answered when deciding whether to renegotiate an upstream SLA or move a job.
loaded_at_field, warehouse metadata, and the two ways freshness fails green
dbt computes freshness by selecting the maximum value of the loaded_at_field and comparing it to the current timestamp, and that field is required to calculate freshness except where dbt can leverage warehouse metadata instead. Both routes have a failure mode that reports fresh when the data is not. The first is a column choice. If loaded_at_field points at a business timestamp such as order_date or event_time rather than the moment the row was written, then a pipeline that stopped loading yesterday still returns a max close to now for as long as late arriving rows trickle in, and a backfill of last quarter will make the table look stale while nothing is wrong. The second is metadata. When dbt falls back to warehouse metadata, on Snowflake it reads LAST_ALTERED, which moves on any operation that touches the table object: a truncate and reload that truncated and then failed, a column addition, a permissions change, a merge that matched zero rows. Each of those reports fresh. The Delta equivalent is that OPTIMIZE and VACUUM write new table versions without writing data, so a raw commit time check reports fresh on a table that has received nothing for a day. On the Fusion engine there is a third case worth knowing: views are treated as always fresh, because freshness cannot be determined from view metadata. If a meaningful part of your source layer is exposed as views, that is a silent hole in the coverage. None of these are bugs. They are the consequence of asking a single timestamp to answer a question about a process.
The filter, the full table scan, and the cost of checking often
Freshness checks are cheap until they are not. Because the query is a max over a column, on a large table without appropriate clustering or partitioning it can turn into a full scan, and dbt supports a filter on the freshness block specifically so the check can be restricted to a recent partition. That is the right fix and it introduces a maintenance coupling: the filter has to stay in step with the partitioning scheme and with how late data can arrive. A filter of one day on a source that sometimes receives data three days late will look fine and will quietly stop seeing the rows that matter. A filter written against a partition column that later changes name or granularity will either error loudly, which is fine, or silently narrow the window, which is not. There is also a scheduling tension underneath. Detection latency is bounded by how often you run the command, so catching a break within fifteen minutes means running the check every fifteen minutes, which multiplies the scan cost across every source. Teams resolve this by checking important tables often and everything else nightly, which is a reasonable compromise and also another tier of configuration to maintain. Monitoring built on metadata and query history rather than repeated max queries removes the tradeoff, because observing that a load happened does not require reading the table.
Getting a stale source in front of a human, which is where most setups stall
Detection without delivery is a report nobody reads. Suppose the check works: the command runs on schedule, thresholds are set, and at 04:30 a source crosses error_after. What happens next is the part teams underestimate. In dbt Cloud the notification triggers available are the outcomes of a job run, which means success, failure or cancellation, rather than model or test level events, and notifications are configured per recipient rather than per job, with Slack delivery limited to public channels. So the granularity you can express is roughly this job went wrong, not the orders source is four hours late and the finance dashboard depends on it. Teams close the gap by parsing target/sources.json in a follow up step and calling a webhook, and that script grows quickly: it needs a mapping from source to owner, deduplication so a table stale for nine hours produces one incident rather than nine runs of noise, suppression during known maintenance, escalation when nobody acknowledges, and somewhere to record that the incident happened so the pattern is visible next quarter. Every piece of that is buildable in a week and maintainable for years. It is also, taken together, a monitoring product, which is why a team that starts with dbt source freshness in March is frequently still maintaining an alerting script in September.
What a freshness incident looks like when the monitoring is doing its job
Take the case that costs the most, because it is the one a freshness check is structurally unable to catch. The daily orders source lands on time at 04:00, and at 09:00 the revenue dashboard is visibly wrong. dbt source freshness reported green all morning, correctly, because rows arrived inside the window it was asked about. Volume caught it: the table gained 3,100 rows against a weekday baseline near 41,000, a shortfall most fixed thresholds ignore because it is not zero. Schema explains it: an upstream system began sending a new value in a status column, and a filter in the staging model dropped everything that did not match the known set. Lineage closes it: the same staging model feeds four dashboards and a reverse ETL sync into the CRM, so the blast radius is known before anyone asks. That sequence is the argument for putting the pillars on one baseline and into one incident rather than into four scheduled queries. Freshness is the cheapest of the four to build and the least likely on its own to catch the failure that damages trust, because the most common serious incident is not that nothing arrived. It is that something arrived and it was wrong.
Questions buyers ask
dbt source freshness FAQ
How do I check source freshness in dbt?
Declare a freshness block on the source in YAML with warn_after, error_after or both, name a loaded_at_field, then run the dbt source freshness command. dbt queries the maximum value of that field, compares it against the current timestamp, and writes the outcome to target/sources.json. Use the -o flag to write the artifact somewhere else.
What happens if warn_after and error_after are not set?
dbt will not calculate freshness for the tables in that source. This is the detail that catches teams out, because nothing fails and no warning appears. The source simply drops out of the freshness check while the job stays green, so a table can go uncovered for months without anyone noticing. Setting freshness to null on a table disables it the same way, deliberately.
Does dbt build run source freshness?
No. dbt build runs models, tests, snapshots and seeds, and source freshness is a separate command. The intended pattern is to run dbt source freshness first, then use the source_status selector so dbt build rebuilds only the models below sources that got fresher. If the freshness step is skipped or fails, the build proceeds on stale data and stamps fresh timestamps on every output.
What is loaded_at_field in dbt?
It is the column dbt reads to decide when data last arrived, using the maximum value of that column against the current timestamp. It is required to calculate freshness except where dbt can use warehouse metadata instead. Choose a column that records when the row was written, not a business timestamp, because a business timestamp reports fresh while loading is broken.
Why does dbt source freshness say there is nothing to do?
Almost always because no source in the selected set has thresholds to evaluate. If neither warn_after nor error_after is set, freshness is not calculated for those tables, so a selector that matches only such sources leaves dbt with no work. Check the hierarchy too, since freshness and loaded_at_field set on a source flow down to its tables and can be overridden or emptied at either level.
Can dbt check freshness on models instead of sources?
Source freshness is designed for sources, which are the tables dbt reads but does not build. For models, the equivalent question is usually whether the model was rebuilt and whether the result looked normal, which is answered by tests and by job outcomes rather than by a freshness window. Monitoring the warehouse directly covers models, sources and tables that are neither.
How do I stop a dbt freshness check from scanning the whole table?
Add a filter to the freshness block so the max is evaluated against a recent partition rather than the full table, for example restricting the load timestamp to the last day. Keep the filter in step with the partitioning scheme and with how late your data can arrive, because a window narrower than real late arrival silently hides the rows that matter.
Does dbt alert me when a source is stale?
Not directly at the table level. The check writes its result to an artifact, and in dbt Cloud the available notification triggers are job run outcomes rather than model or test events, configured per recipient rather than per job, with Slack limited to public channels. Reaching an on call rotation with the name of the stale table means parsing the artifact and calling a webhook yourself.
How much does dbt source freshness monitoring cost with Dataobservability?
Plans start at 99 dollars a month for Starter, 299 for Team and 799 for Scale, with a 14 day trial and no card required. Pricing is published rather than quoted, and it does not meter per monitor or per monitored table, so covering more sources and more schemas does not increase the bill. Snowflake, BigQuery, Databricks and Redshift are included at every tier.
More of the platform
Catch broken data before your stakeholders do
Connect your warehouse and get dbt source freshness live from one read-only connection. Transparent pricing, no credit card.