• new

    Release 2026.06 - Bringing Data Observability Into Your Code

  • new

    Contribute to the Future of AI & Data Innovation

  • new

    • Release 2026.06 - Bringing Data Observability Into Your Code

  • new

    • Contribute to the Future of AI & Data Innovation

Dbt Source Freshness: Prevent Stale Data in 2026

|

7

min read

You usually notice stale data when someone else notices it first.

A finance lead pings you because yesterday's numbers are still on today's dashboard. The orchestrator shows green. dbt models built successfully. Nothing looks broken until you trace the issue back upstream and realize the source table stopped receiving new records. That's the uncomfortable part of timeliness problems. They often fail unnoticed.

Many teams learn this lesson the same way. They invest in transformation tests, documentation, and lineage, but they don't add a direct check for whether raw data is still arriving on time. If you're already working through what stale data means in practice, dbt source freshness is one of the cleanest ways to close that gap. It gives you a simple operational answer to a high-value question: has this source updated when it was supposed to?

That matters because stale data rarely stays isolated. A delayed ingestion can flow into executive dashboards, revenue models, ML features, and audit-facing reports before anyone realizes the source is old. If you're tightening your process around identifying data reliability flaws, freshness checks belong near the top of the list. They don't replace quality tests, but they catch a class of failure that standard schema and logic tests won't.

Table of Contents

The Problem with Stale Data

The most frustrating data incidents aren't always loud. A job can complete, logs can look normal, and downstream models can still materialize on top of outdated inputs.

That's why stale data causes trust problems so quickly. The business user doesn't care whether Airflow, Dagster, or dbt technically “ran.” They care that the metric on the dashboard matches reality. If a payments source stopped loading overnight, a healthy transformation run just republishes old truth.

Silent failures are the expensive ones

A typical failure chain looks like this:

  • An upstream extractor stalls. The source table keeps yesterday's latest timestamp.

  • dbt still runs downstream models. Transformations succeed because the table exists and the schema still matches.

  • Dashboards update on schedule. Stakeholders assume the data is current because the BI layer refreshed.

  • Trust drops fast. Once someone spots the gap, every number near that dashboard becomes suspect.

This is exactly the problem dbt source freshness is built to catch. It checks whether a source has been updated recently enough to match the cadence you expect.

Freshness issues rarely start as “bad SQL.” They usually start as “data stopped arriving and nobody was watching the clock.”

Why this issue slips through mature teams

Teams often have strong test coverage for nulls, uniqueness, relationships, and accepted values. Those checks matter, but they answer different questions. A source can pass every quality test and still be stale.

That distinction matters most for operational data. Revenue, payments, marketing spend, inventory, and clinical or compliance-sensitive reporting all depend on timeliness, not just correctness. If the latest row is old, perfectly modeled downstream tables still tell the wrong story.

The practical fix is straightforward. Put a freshness contract on the raw sources that matter, wire the result into orchestration, and stop the pipeline when the source has missed its expected arrival window. That turns stale data from a reactive support ticket into an actionable engineering signal.

Understanding dbt Source Freshness

dbt source freshness is a built-in way to monitor whether source tables are arriving on time. It's been a core dbt feature since 2019, and it works by comparing the most recent timestamp in a loaded_at_field against configurable warn_after and error_after thresholds. If the data is older than the allowed period, the dbt source freshness command exits with a non-zero code, which can fail CI jobs and block downstream work, as documented in dbt source testing.

A diagram explaining dbt Source Freshness, covering its definition, limitations, and key configuration components for data observability.

If you need a business-facing explanation of the concept, this guide to data freshness and why it matters for business decisions is a useful companion to the implementation details.

What it is

dbt source freshness functions as an SLA check for raw data arrival.

You define a source in sources.yml, point dbt at the timestamp column that represents when records landed, and tell it how old that source is allowed to get before you want a warning or an error. dbt then queries the maximum value of that field and compares it to the current time.

That makes it very effective for sources with a known cadence. If a sales table should update daily, a freshness error tells you the latest data is older than that accepted window.

What it isn't

dbt source freshness isn't a full quality framework by itself.

It doesn't tell you whether values are correct, whether a primary key is duplicated, whether a foreign key still resolves, or whether business logic changed upstream. It also doesn't natively understand nuanced arrival behavior beyond the threshold you define.

That's why I think of it as a smoke detector, not a fire investigation. It tells you that the stream may have stopped flowing. It doesn't explain every reason why.

Practical rule: Use freshness checks for timeliness, then use dbt tests and other monitoring for correctness, completeness, and drift.

The three pieces that matter most

When someone is setting this up for the first time, I tell them to focus on three configuration decisions:

  1. Pick the right loaded_at_field
    This field should reflect data arrival or load recency, not an arbitrary business event timestamp.

  2. Set realistic thresholds
    warn_after should tell you the source is trending late. error_after should tell your pipeline to stop trusting it.

  3. Choose the right scope
    Not every source deserves the same level of monitoring. Start with the tables that feed high-visibility dashboards, finance reporting, or ML features.

Get those three right and dbt source freshness becomes operationally useful very quickly.

Configuring Freshness Checks in Your Project

The setup lives in your source YAML. That's good news because it keeps timeliness rules close to the source definitions your team already maintains.

The dbt source freshness command relies on a freshness block in source YAML, where you define warn_after and error_after with a period and count such as {count: 1, period: day}. When it runs, dbt compares the max value of the loaded_at_field to the current time and saves results to target/sources.json, which makes the output easy to feed into other tooling and visualizations, as shown in dbt freshness configuration documentation.

A dbt source configuration yaml file illustrating source freshness settings for user and order tables.

A practical YAML example

Here's a clean pattern you can adapt:

version: 2

sources:
  - name: stripe
    schema: raw_stripe
    tables:
      - name: charges
        loaded_at_field: _etl_loaded_at
        freshness:
          warn_after: {count: 30, period: minute}
          error_after: {count: 60, period: minute}

      - name: payouts
        loaded_at_field: _etl_loaded_at
        freshness:
          warn_after: {count: 12, period: hour}
          error_after: {count: 24, period: hour}

  - name: salesforce
    schema: raw_salesforce
    tables:
      - name: opportunities
        loaded_at_field: _etl_loaded_at
        freshness:
          warn_after: {count: 1, period: day}
          error_after: {count: 2, period: day}
version: 2

sources:
  - name: stripe
    schema: raw_stripe
    tables:
      - name: charges
        loaded_at_field: _etl_loaded_at
        freshness:
          warn_after: {count: 30, period: minute}
          error_after: {count: 60, period: minute}

      - name: payouts
        loaded_at_field: _etl_loaded_at
        freshness:
          warn_after: {count: 12, period: hour}
          error_after: {count: 24, period: hour}

  - name: salesforce
    schema: raw_salesforce
    tables:
      - name: opportunities
        loaded_at_field: _etl_loaded_at
        freshness:
          warn_after: {count: 1, period: day}
          error_after: {count: 2, period: day}
version: 2

sources:
  - name: stripe
    schema: raw_stripe
    tables:
      - name: charges
        loaded_at_field: _etl_loaded_at
        freshness:
          warn_after: {count: 30, period: minute}
          error_after: {count: 60, period: minute}

      - name: payouts
        loaded_at_field: _etl_loaded_at
        freshness:
          warn_after: {count: 12, period: hour}
          error_after: {count: 24, period: hour}

  - name: salesforce
    schema: raw_salesforce
    tables:
      - name: opportunities
        loaded_at_field: _etl_loaded_at
        freshness:
          warn_after: {count: 1, period: day}
          error_after: {count: 2, period: day}

This example does three useful things:

  • It uses one load timestamp convention. _etl_loaded_at is explicit and easy to standardize across ingestion tools.

  • It sets different thresholds per table. Charges and payouts don't have the same arrival pattern, so they shouldn't share the same freshness rule.

  • It keeps rules readable. Anyone reviewing the project can understand the operational expectation without reading external documentation.

How to choose thresholds

The mistake I see most often is copying the same values everywhere. Freshness thresholds should reflect actual ingestion behavior and business tolerance.

Source pattern

Good warning threshold

Good error threshold

Why

Near-real-time event stream

30 minutes

60 minutes

Gives you room for short ingestion jitter without ignoring a real outage

Hourly batch feed

30 minutes after expected lag

1 hour after expected lag

Catches delay before dashboards are materially behind

Daily business system export

1 day

2 days

Useful when the source updates once per business cycle

Low-priority reference data

Qualitative, based on use

Qualitative, based on use

Some dimensions don't need aggressive alerting

Use this table as a decision aid, not a template to apply blindly.

What works in real projects

A few habits make setup smoother:

  • Prefer ingestion timestamps over business timestamps. created_at might describe when a customer acted. It doesn't always describe when the row landed in your warehouse.

  • Keep freshness close to ownership. If one team owns a source, make sure they agree with the thresholds in code review.

  • Document exceptions. Some sources arrive late by design on weekends, month-end, or after external vendor processing.

If you start with your most important raw sources and get those definitions right, the first rollout of dbt source freshness usually takes less effort than the debugging it prevents later.

Running Checks and Interpreting Results

Once the YAML is in place, you can run the check with the command your team will use constantly:

dbt source freshness

You don't need to run it across everything every time. dbt also supports node-specific execution such as dbt source freshness --select source:stripe.charges, which is useful when you're debugging a volatile table or validating a change without the overhead of a full project-wide check.

A digital display showing a dbt source freshness check result with all data sources marked as fresh.

What the statuses mean

The output is simple, and that's one reason it fits well into CI and orchestration:

  • PASS means the latest timestamp is inside your acceptable threshold.

  • WARN means the source is older than warn_after, but not yet past error_after.

  • ERROR means the source crossed the failure threshold and should be treated as stale.

That WARN state is useful operationally. It gives you an early signal that a source is trending late, while still letting you decide whether to halt dependent work.

Why the exit code matters

The command's exit code is what turns freshness from a dashboard into a control point.

If a source fails freshness, dbt exits non-zero. That lets GitHub Actions, GitLab CI, Airflow, Dagster, or dbt Cloud jobs stop downstream execution automatically. In practice, that means you can prevent stale source data from propagating into marts, dashboards, and exports.

A freshness check that only writes a log is observability. A freshness check that blocks bad downstream runs is an operating control.

Scheduling strategy

A practical rule from Secoda's dbt source freshness guidance is to run checks at least twice as frequently as the expected data cadence. Their example is every 30 minutes for hourly loads. That gives you enough coverage to catch a missed update before the stale data becomes visible downstream.

A good lightweight workflow looks like this:

  1. Run freshness before transformation jobs for critical sources.

  2. Fail fast on error so stale inputs don't feed trusted models.

  3. Route WARN results to alerts so engineers can investigate early.

  4. Use selective runs in development when testing one source or one ingestion path.

This is one of those controls that's small in code and large in impact.

Common Pitfalls and Advanced Techniques

Most dbt source freshness setups fail for boring reasons, not exotic ones. The command works. The configuration is valid. But the operational result is noisy, slow, or misleading.

The biggest trap is assuming any timestamp column will do. It won't.

An infographic titled Common Pitfalls and Advanced Techniques for managing dbt source freshness in data engineering.

The failure modes that show up first

A useful summary from advanced dbt freshness implementation notes highlights three common issues. Non-indexed timestamp columns can increase query latency by 40 to 60 percent, timezone mismatches can cause false alerts, and omitting a filter on large tables can lead to a 25 percent higher incidence of undetected stale data.

Those aren't academic edge cases. They show up quickly in production.

  • Wrong timestamp semantics
    If loaded_at_field points to an event timestamp instead of a warehouse arrival timestamp, dbt may report a table as fresh even when ingestion is delayed.

  • Timezone confusion
    If your records are stored in UTC but your comparison logic effectively runs against local system time, you can create false stale alerts that train the team to ignore freshness failures.

  • Heavy scans on historical tables
    On large raw tables, a freshness query without a useful filter can waste compute and still miss the operational story you care about right now.

What to change when the basics are stable

Once the core checks are reliable, you can use dbt more selectively.

One useful pattern is to build only off sources that are newly updated. The source_status:fresher+ selector helps trigger downstream models from sources that changed, which is a good fit when your project contains a mix of high-frequency and low-frequency upstream systems.

Don't treat every freshness check as a binary alarm. Some should be gating rules. Others should be routing rules for selective builds.

A practical hardening checklist

When a team asks me to review their setup, I usually check these items first:

  • Index or optimize the timestamp path where your warehouse supports it, especially on large source tables.

  • Standardize on UTC for stored load timestamps, then keep comparison behavior explicit.

  • Add a filter for very large history tables so dbt evaluates the recent operational window rather than scanning irrelevant history.

  • Tune thresholds after observing actual arrival behavior instead of locking them on day one.

  • Separate critical from noncritical sources so one flaky, low-value feed doesn't block trusted delivery everywhere.

dbt source freshness is easy to enable. Making it dependable takes a little more engineering judgment.

Beyond Freshness Building a Full Observability Workflow

Static freshness thresholds solve an important problem, but they don't solve the whole timeliness problem.

A source can arrive “on time” according to error_after and still be late enough to disrupt analysts, executive reporting, or downstream feature generation. That gap becomes clearer in real production systems where delays aren't evenly distributed. The average interval might look fine while rare, severe slowdowns keep surfacing at exactly the wrong time.

Research summarized in Paradime's source freshness best practices points to this underserved issue directly: 70% of stale data incidents stem from delays exceeding 3x the average interval, which is why average-based or even percentile-based threshold setting can miss important tail-delay events.

Screenshot from https://digna.ai

Where dbt stops

dbt source freshness asks a direct question: is the latest timestamp older than the threshold?

That's useful, but limited. It won't by itself tell you whether a feed that usually lands at a predictable time is trending late today, whether delays are worsening over time, or whether a source is still technically “fresh” but already violating the expectation your business users operate against.

Here, broader observability starts to matter. You're no longer just testing staleness. You're monitoring timeliness behavior.

What mature teams add next

Teams usually expand in three directions:

  1. Timeliness monitoring
    They track when data is expected to arrive, not just whether it is currently stale.

  2. Pattern learning
    They compare today's arrival against historical norms instead of relying only on fixed thresholds.

  3. Integrated response
    They connect freshness, schema, anomaly, and validation signals so incident response starts with context.

One example is data observability workflows built around in-database monitoring. The verified capability that matters here is that digna's timeliness component monitors data arrival against learned patterns and user schedules, and it executes analyses inside the customer's database rather than through external polling, as described on digna's timeliness overview. That makes it a different layer from dbt's static threshold check, not a replacement for it.

A practical maturity model

The progression usually looks like this:

Stage

What you monitor

Main limitation

Basic

Static source thresholds in dbt

Misses nuanced arrival drift

Controlled

dbt freshness tied to CI and orchestration

Still mostly reactive

Mature

Timeliness patterns, delays, and expected delivery behavior

Requires broader observability design

The useful mental model is simple. dbt source freshness tells you when a source is stale. Timeliness monitoring helps you catch when it is becoming late.

That difference matters most in environments where a single missed or delayed load has outsized impact, especially when tail delays are rare enough to evade simplistic thresholds but common enough to keep breaking trust.

If your team already uses dbt source freshness and wants broader visibility into late arrivals, record-level validation, and in-database observability workflows, digna is one option to evaluate alongside your existing stack.

Share on X
Share on X
Share on Facebook
Share on Facebook
Share on LinkedIn
Share on LinkedIn

Meet the Team Behind the Platform

A Vienna-based team of AI, data, and software experts backed

by academic rigor and enterprise experience.

Meet the Team Behind the Platform

A Vienna-based team of AI, data, and software experts backed by academic rigor and enterprise experience.

Product

Integrations

Resources

Company