DBT OBSERVABILITY
dbt Observability: dbt Cloud and dbt Core Observability Tools, Monitoring and Data Quality Compared
Monitoring that reads your dbt project and then keeps watching the tables between runs, covering freshness, volume, schema, and distribution with column level lineage. Read only connection, no agent to deploy, 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
What is dbt observability?
dbt observability is the practice of knowing whether the models, sources, and tests in a dbt project are producing correct, fresh, complete data, and being told quickly when they are not. dbt itself supplies four of the pieces: data tests that assert rules you write, source freshness checks that compare a load timestamp against a threshold you set, run artifacts and logs from every invocation, and a lineage graph in dbt Catalog. All four share one property that decides how far they get you. They are run scoped, meaning they only know what happened while dbt was executing. Nothing in dbt watches a table between runs, dbt does not keep a queryable history of test results because stored failures are replaced on each run, and notifications fire at the granularity of a job rather than a model or a test. Dataobservability reads your manifest to inherit the project you already have, then monitors the tables themselves on its own schedule, so a run that never started is an alert instead of silence. Plans are 99, 299, and 799 dollars a month with a 14 day trial and no card required.
Last updated August 2026
Side by side
dbt observability compared
Swipe to see all columns →
| dbt observability capability | What it covers | What you configure | Documented behavior and limits (dbt docs and pricing page, August 2026) |
|---|---|---|---|
| dbt data tests | Assertions you write. Four generic tests ship with dbt: unique, not_null, accepted_values and relationships, plus singular tests written as SQL in the tests directory | A data_tests block per column or model, and severity per test | A test passes when the query returns zero failing rows. severity is error by default and can be set to warn, with error_if and warn_if thresholds. store_failures writes failing rows to a schema named or suffixed dbt_test__audit, and the docs are explicit that a test result always replaces the previous failures for that test, so there is no cumulative history to trend |
| dbt source freshness | How long ago a source table was loaded, compared against a threshold you choose | A freshness block per source with warn_after and error_after, each taking a count and a period, plus loaded_at_field on the table | If neither warn_after nor error_after is provided, dbt will not calculate freshness for the tables in that source at all. dbt build does not include source freshness, so it has to be run as its own dbt source freshness step or ticked as a separate execution setting. On Snowflake the calculation uses LAST_ALTERED, which moves for reasons other than new rows arriving |
| dbt Fusion state aware orchestration | Source freshness tracked from warehouse metadata without a configured threshold | Running the Fusion engine | Fusion tracks source freshness using warehouse metadata, so warn_after and error_after are not required for it to detect that source data changed. The documented caveat is that Fusion treats views as always fresh, because it cannot determine freshness from view metadata |
| dbt Catalog lineage, formerly dbt Explorer | A model level lineage graph for the project, and column level lineage on the plans that include it | Nothing beyond running jobs in dbt platform, on a qualifying plan | Column level lineage is an Enterprise and Enterprise Plus feature. The docs list its limits plainly: it reflects lineage from select statements and does not reflect other usage like joins and filters, it fails to parse complex lateral joins and JSON unpacking, it cannot determine lineage through Python models at all, and hardcoded table names used instead of ref produce unknown errors |
| dbt Cloud job notifications | Telling people that a job run finished in a given state | Each recipient picks the jobs, environments and statuses they want | Channels are email, Slack and Microsoft Teams, with PagerDuty reached through an external email address. Triggers are succeeds, warns, fails and is canceled, which are job run outcomes rather than model or test events. Notifications are configured per recipient rather than per job, Slack integrations support public channels only, and an account links one Slack workspace |
| Elementary, re_data and similar dbt packages | A results store and anomaly detection built from dbt artifacts, installed into the project | A package in packages.yml, models to materialize the results, and somewhere to host the report | These fill the missing result store genuinely well and inherit the same boundary, because a package that runs as part of a dbt invocation does not run when dbt does not. Elementary is Apache 2.0 and requires dbt; its paid tiers are quote only |
| Dataobservability | Freshness, volume, schema and distribution on every table in the warehouse, column level lineage, grouped incidents, and monitors generated from your manifest | A read only warehouse connection and your dbt project | Covers Snowflake, BigQuery, Databricks and Redshift plus dbt. Systems outside those connections are not monitored. Pricing is published at 99, 299 and 799 dollars a month |
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 tests and dbt Cloud
Monitoring that runs when dbt is not running
This is the whole argument in one line, and it is a property of the design rather than a gap somebody forgot to fill. Every observability signal dbt produces is generated during an invocation: tests run inside dbt test or dbt build, freshness runs inside dbt source freshness, artifacts are written when a run ends. Between those runs dbt knows nothing, because dbt is not a process that is running. Now rank the ways a table actually goes wrong in production. The job was paused during a deploy and never unpaused. The scheduler credentials expired. An upstream vendor silently stopped delivering and the source table simply stopped growing. Someone ran a manual backfill that rewrote six months of history. A stakeholder is reading a dashboard built on a table nobody has rebuilt since Thursday. Not one of those fails a dbt test, because no dbt test ran, and a project with no failing tests looks exactly like a project with no problems. Watching the tables on an independent schedule is what converts that silence into an alert.
A result history you can actually query
dbt has a store_failures config and it is genuinely useful, and it is not a history. The documentation states the behavior directly: a test result always replaces the previous failures for the same test. So the audit table tells you which rows are failing right now, and it cannot tell you whether this column has been degrading for three weeks, how often this model has been late this quarter, or what the row count normally is on a Tuesday. Every question with the word normally, drifting or how often in it needs results that accumulate, with a schema and retention behind them. This is the specific gap that sends teams to Elementary or re_data, and it is worth being clear that what they are usually adopting is the result store rather than the assertions. Keeping a metric history per table by default removes that build, and it is also the only way a baseline can exist at all.
Thresholds learned from the table instead of chosen by a person
Look at what dbt asks you to supply. warn_after and error_after each take a count and a period. accepted_values takes a list. A singular test comparing today against yesterday takes whatever ratio you typed. Every one of those numbers is a guess that made sense the week it was written and ages from there. Set freshness tight and it warns every Monday morning when the weekend load lands late by design. Set it loose and it misses the day the pipeline delivered forty percent of the usual rows. There is no seasonality in a threshold, so the month end spike, the quiet Sunday and the Thanksgiving week all have to be encoded by hand or tolerated as noise. Baselines built from each table's own load history handle all three without anybody writing a rule, and they get better as the table accumulates history rather than drifting out of date.
Coverage that is not the count of tests somebody wrote
dbt tests cover exactly the columns and models a person decided to add a data_tests block to. That is correct behavior for an assertion framework, and it makes coverage a manual inventory rather than a default. In practice test coverage in a mature project is deep on the twenty models that broke memorably and thin to absent across the long tail, and the long tail is where the surprises live: the model added eighteen months ago by a team that has since reorganized, the source nobody owns, the staging table three people quietly depend on. Reading the warehouse catalog and monitoring what is actually there inverts that. New table appears, it is monitored. No pull request, no review from whoever understands the DAG, no deploy window.
Schema drift on columns no test mentions
A vendor adds a field, an upstream job retypes an amount from numeric to varchar, a column is dropped and a downstream model resolves it to null. A not_null test on the columns you named will not notice any of that, and the retype is the dangerous one, because the test that reads the column can still pass while every aggregate below it is now wrong. Watching the catalog for adds, drops and type changes is a different mechanism from asserting values, and it is the one that catches breakages nobody predicted. Since unpredicted breakages are most of them, that matters more than the coverage numbers suggest.
Alerts at the model, with incident state
dbt Cloud notifications fire on job run outcomes, and the documented triggers are succeeds, warns, fails and is canceled. A four hundred model job that fails one test on one staging table produces the message that the job failed. Someone then opens the run, reads the log, and works out which of the four hundred it was. Notifications are also configured per recipient rather than per job, so routing a specific schema to the team that owns it means asking that team to configure themselves in. And because a failure repeats on every scheduled run until it is fixed, a table stale since Friday sends the same alert Saturday, Sunday and Monday until someone mutes the channel. Grouping repeated failures into one incident with an owner and an acknowledgement is what makes the difference between a page that gets answered and a rule somebody writes a filter for.
How it works
From connected to caught
Point it at the dbt project you already have
Dataobservability reads your manifest, so the models, sources, tests and descriptions you have already written become the starting inventory rather than something to re enter. Monitors are generated for the models and sources it finds, and the project graph seeds lineage. Nothing changes in the repository: no package added to packages.yml, no models materialized into your warehouse, no job step inserted, no schema created. That matters beyond convenience, because a monitoring layer that needs a pull request and a deploy to change is a monitoring layer that stops changing.
Connect the warehouse read only, so checks survive a bad week for dbt
Create a user with USAGE on the schemas and SELECT on the tables in Snowflake, BigQuery, Databricks or Redshift. Monitoring reads catalog metadata and the warehouse's own statistics to build the freshness and volume picture, so nothing is copied out and no full table scans land on your bill. This is the step that decouples the checks from the scheduler. Once the warehouse connection exists, a paused job, a failed run, an expired credential or a source that quietly stopped delivering all produce an alert, because the thing being watched is the table rather than the invocation.
Let baselines build before you tune anything
Give it a few days of load history per table so weekly seasonality becomes visible, and resist the urge to adjust thresholds in week one. Tuning before a baseline exists is how teams manufacture alert fatigue immediately and stop trusting the monitoring by week two. Keep your dbt tests running throughout. They fail differently, which is the useful part: your tests will catch the business rule violations you encoded, and the baselines will catch the loads that did not happen and the distributions that shifted.
Keep dbt tests for the invariants and stop writing freshness by hand
The tests worth keeping are the ones a learned baseline can never know. That a status column only ever holds one of five values. That a foreign key resolves. That a discount never exceeds forty percent. Those are business rules, they belong in the repository next to the transformation that produces them, and accepted_values and relationships express them cleanly. What is worth handing over is the per source freshness config, the row count sanity checks, the null rate assertions and the hand rolled anomaly SQL, because those are the ones that need a history and a distribution to be any good. That split keeps the project readable instead of accumulating a test per column until nobody can find the models.
Route by owner, then prune honestly after two weeks
Send alerts for each schema to the team that owns it rather than into one shared data alerts channel everybody has already muted. Then read the log back with some honesty. A monitor that has never fired on a real problem should be relaxed or removed. Any incident that reached you because a stakeholder complained should become a monitor. Monitoring that is never pruned becomes noise, and noise gets muted, generally by the person you most needed to reach.
What dbt observability actually means, and the four things dbt ships
The phrase gets used two ways and it is worth separating them, because the answer to which tool you need depends entirely on which one you mean. The first is observability of dbt: is the project healthy, are runs succeeding, which models are slow, which tests failed last night, how much is this costing. The second is observability of the data dbt produces: is the output fresh, complete, correctly shaped and trustworthy enough to put in front of a stakeholder. dbt is strong at the first and partial at the second, and most disappointment comes from expecting the second and receiving the first. Concretely, dbt gives you four things. Data tests are assertions you write, and four generic tests ship out of the box: unique, not_null, accepted_values and relationships. A test is a select statement that returns failing rows, and it passes when it returns none. Source freshness compares a load timestamp against a threshold, configured per source with warn_after and error_after, each taking a count and a period. Run artifacts, principally manifest.json and run_results.json, describe what the project contains and what happened during an invocation, and everything built on top of dbt observability, including the packages, is ultimately built on those two files. And dbt Catalog, which was renamed from dbt Explorer, renders the lineage graph and adds column level lineage on Enterprise and Enterprise Plus. That is a genuinely good foundation, better than most transformation tools ship, and it is a foundation rather than a monitoring system.
The run scoped boundary, and why it decides everything else
Every one of those four signals is produced during an invocation. Tests execute inside dbt test or dbt build. Freshness executes inside dbt source freshness, and it is worth knowing that dbt build does not include source freshness, so it has to be added as its own step or enabled as a separate execution setting. Artifacts are written when a run completes. Catalog reflects the state as of the last run that populated it. Draw the timeline and the shape of the problem is obvious: there are bars where dbt is executing and gaps between them, and every signal lives inside a bar. The gaps are where most data incidents happen. A source that stops delivering does not fail a test, it just stops the source table growing, and unless a freshness check is configured on that specific source with an explicit threshold, nothing looks at it. And here the documentation contains a detail that catches a lot of teams: if neither warn_after nor error_after is provided, dbt will not calculate freshness for the tables in that source at all. Freshness is not on by default and quietly absent, it is absent unless a person configured a number per source. The dbt Fusion engine improves this genuinely, tracking source freshness from warehouse metadata under state aware orchestration so the thresholds are not required, and it carries its own documented caveat that Fusion treats views as always fresh because it cannot determine freshness from view metadata. There is a related warehouse level trap worth naming: on Snowflake, freshness is calculated from LAST_ALTERED, and on Delta tables an OPTIMIZE or VACUUM writes a new version without writing new data, so a naive last commit time reads fresh on a table that has received nothing for days.
Why store_failures is not a history, and what that costs you
This is the most consequential sentence in the dbt testing documentation and it is easy to read past: a test result always replaces the previous failures for the same test. store_failures is a debugging aid, writing the currently failing rows into a schema named or suffixed dbt_test__audit so you can select from them instead of scrolling a log. It is not a record. Every run overwrites it. Follow that through and a whole category of question becomes unanswerable from dbt alone. Has the null rate on this column been creeping up for a month, or did it break this morning. Is this table late more often than it used to be. What is the normal row count for a Tuesday, and is today outside the range. How many incidents did this domain have last quarter, and is the trend improving. None of those can be answered from a table that is truncated on every run, and all of them are the questions a data team gets asked when it starts being taken seriously. There is a second, subtler cost. Without a history there is no distribution, and without a distribution there is no baseline, so every threshold in the project has to be a constant somebody chose. That is why so much hand rolled dbt anomaly detection ends up as a singular test comparing today to yesterday with a ratio in it: it is the most you can do with one prior data point. This is also precisely the hole the dbt native packages exist to fill. Elementary and re_data both work by materializing artifacts and test results into models in your warehouse so that history accumulates and anomaly detection becomes possible. They do it well, they are the right first step for a lot of teams, and they inherit the boundary from the previous section without changing it, because a package that runs as part of a dbt invocation does not run when dbt does not.
Lineage in dbt Catalog: what it covers, what it costs, and where the parser gives up
dbt Catalog, renamed from dbt Explorer, gives every project a model level lineage graph, and on Enterprise and Enterprise Plus it adds column level lineage with no extra setup. Column level lineage is the capability people actually want during an incident, because the question in an incident is never which models are downstream, it is whether this specific broken column reaches anything a person is looking at. So it is worth knowing exactly what dbt documents about its limits, because they are specific and they are not marketing hedges. First, it reflects the lineage from select statements in your models SQL and does not reflect other usage like joins and filters. A column that only ever appears in a join condition or a where clause is doing real work and will not show up as a dependency. Second, it relies on SQL parsing, and parsing fails in named cases. Complex lateral joins are called out as ambiguous. JSON unpacking is called out. Python models cannot be parsed at all, because of the nature of Python models it is not possible to determine the lineage, so a Python model is a hole in the graph rather than a node with edges. And hardcoded table names used instead of ref produce an unknown error, which in a project of any age is more common than anyone would like. Third, and this is the commercial part, it is Enterprise only, so a team on Developer or Starter has model level lineage and nothing below it. The alternative is to build lineage from what the warehouse itself records, which is a different mechanism with a different failure profile: Snowflake exposes ACCESS_HISTORY with objects_modified on Enterprise and above, Databricks Unity Catalog captures column level lineage automatically but nothing before September 1 2024 and nothing preserved through renames, BigQuery covers top level columns only and falls back to table level above 1,500 column links per job, and Redshift has no native column level lineage graph at all. Reading query history rather than parsing project SQL means the Python model, the hardcoded table name and the lateral join are all visible, because they all left a trace in the warehouse when they ran.
dbt Cloud pricing, and what observability actually costs on each tier
dbt publishes its prices, which puts it ahead of most of this category, and the meter is worth understanding because it interacts with observability in a way that surprises people. Checked on the pricing page in August 2026: Developer is free with one developer seat and 3,000 successful models built per month. Starter is 100 dollars per user per month, covering five developer seats and 15,000 successful models built per month. Enterprise is custom pricing with a custom seat count and 100,000 successful models built per month, and Enterprise Plus is also custom, at the same 100,000 models with unlimited projects. Two consequences follow. The first is that the unit is models built, so anything that increases how often you build increases the bill. Running more frequently to catch problems sooner is a direct cost, and it is a poor way to buy detection anyway, since building every hour to find a stale source is paying warehouse compute and dbt consumption to answer a question a metadata read answers for nothing. The second is that column level lineage sits behind Enterprise, so the honest comparison for a team on Starter is not dbt versus a monitoring tool, it is the Enterprise upgrade versus a monitoring tool, and those are different numbers. It is also worth putting the rest of the market beside it, because published pricing is rare here and the comparison is usually made without numbers. Soda publishes a Team plan at 750 dollars a month. Metaplane, now part of Datadog, publishes a free tier of 10 monitored tables, 4 users and 3 custom SQL monitors, a Pro tier at 100 monitored tables and 5 custom SQL monitors, and a custom Enterprise tier with unlimited tables and 10 custom SQL monitors, so custom SQL monitors stay in single digits at every tier. Monte Carlo publishes no list price on its own site, and its AWS Marketplace listing shows a 12 month contract at 50,000 dollars against a credit dimension with overage at one cent per unit. Bigeye pricing page returns a 404, and its AWS Marketplace listing shows 45,000 dollars a year for 100 actively monitored tables and 75,000 dollars for 300. Elementary, Sifflet, Anomalo, Acceldata and Datafold are all quote only. Dataobservability is 99, 299 and 799 dollars a month with a 14 day trial and no card, which means you can hold a real number against the Enterprise upgrade quote before anyone books a call.
Choosing: dbt tests only, a dbt native package, or a monitoring layer
There are three honest answers and the right one depends on the shape of your estate rather than on how much you like dbt. If everything that matters flows through dbt, you have perhaps a few dozen models, an engineer maintains them attentively, and the risks you lose sleep over are business rule violations rather than outages, then dbt tests plus a configured freshness block on your real sources is the right amount of tooling. It costs nothing extra, it lives in the repository, it is reviewed like code, and you should stop there. The line moves when you start needing history. The tell is a specific kind of question arriving from outside the team: is this getting worse, how often does this happen, was last month better. At that point a dbt native package is the natural next step, because Elementary and re_data materialize artifacts and test results into the warehouse and give you accumulation, anomaly detection built on it, and a report to share, without leaving the dbt idiom. They are Apache 2.0, they install through packages.yml, and for a dbt only stack they are frequently the correct choice. The line moves a second time when either of two things becomes true. Coverage stops being a curated list and becomes the goal, meaning you want the tables nobody instrumented watched too, which no package can do because packages only see the project. Or your warehouse contains data that dbt did not produce, which is nearly always the case: raw ingestion landing from Fivetran or Airbyte, reverse ETL outputs, tables written by a Spark job, anything a different team owns. dbt sees its own DAG, and the incident is often upstream of the first model. At that point what you need is something that reads the warehouse rather than the project, keeps history by default, learns baselines per table, watches schema, and runs on a schedule that does not depend on the scheduler being healthy. That is a system rather than a set of checks, and the build estimate for it is consistently wrong in the same direction, because the checks are the easy part and the result store, the baselines, the seasonality, the incident state and the routing are the rest of it.
Questions buyers ask
dbt observability FAQ
What is dbt observability?
It is knowing whether the models, sources and tests in a dbt project are producing correct, fresh and complete data, and being told quickly when they are not. dbt supplies four pieces: data tests, source freshness checks, run artifacts and logs, and the lineage graph in dbt Catalog. All four are run scoped, meaning they are produced while dbt is executing, so they describe what happened during a run rather than the state of your tables between runs.
Does dbt Cloud have data observability built in?
Partly. dbt Cloud ships run history and logs, job notifications on succeeds, warns, fails and is canceled, source freshness results, and lineage in dbt Catalog with column level lineage on Enterprise and Enterprise Plus. What it does not ship is monitoring that runs when dbt is not running, a queryable history of test results, learned baselines instead of thresholds you type, or alerting at the level of an individual model or test rather than a job run.
What are the four generic tests that ship with dbt?
unique, not_null, accepted_values and relationships. Each is a parameterized query you attach to a column or model in YAML, and a test passes when it returns zero failing rows. Anything beyond those four is either a singular test written as SQL in the tests directory, a generic test you define yourself, or a package such as dbt_utils or dbt_expectations that ships additional generic tests.
Does dbt keep a history of test results?
No. store_failures writes failing rows to a schema named or suffixed dbt_test__audit, and the documentation states that a test result always replaces the previous failures for the same test. So you can inspect what is failing now and you cannot trend it. Answering whether a null rate has been drifting, how often a model has been late, or what a normal row count looks like requires results that accumulate somewhere, which is what packages like Elementary and re_data or a monitoring platform provide.
Why is my dbt source freshness check not running?
Two common causes, both documented. First, dbt build does not include source freshness, so unless dbt source freshness is added as its own run step or enabled as a separate execution setting, it never executes. Second, freshness requires a threshold: if neither warn_after nor error_after is provided in the freshness block, dbt will not calculate freshness for the tables in that source at all. A source with a loaded_at_field but no thresholds is silently unmonitored.
Is column level lineage available in dbt Core?
No. Column level lineage is a feature of dbt Catalog, formerly dbt Explorer, and it is available on Enterprise and Enterprise Plus plans. dbt Core produces the manifest that lineage is built from, and rendering column level lineage from it is either an Enterprise feature, a third party tool, or something you build against warehouse query history such as Snowflake ACCESS_HISTORY or Databricks Unity Catalog.
What are the limitations of dbt column level lineage?
The docs list them specifically. It reflects lineage from select statements and does not reflect other usage like joins and filters, so a column used only in a join condition is invisible. SQL parsing fails on ambiguous or complex cases, with complex lateral joins and JSON unpacking named directly. Python models cannot be parsed at all, so they are a hole in the graph. And hardcoded table names used instead of ref produce unknown errors.
How much does dbt Cloud cost?
Checked on the dbt pricing page in August 2026: Developer is free with one seat and 3,000 successful models built per month. Starter is 100 dollars per user per month with five developer seats and 15,000 successful models built per month. Enterprise is custom pricing with 100,000 successful models built per month, and Enterprise Plus is also custom, adding unlimited projects. The meter being models built matters for observability, because running more often to detect problems sooner increases the bill.
Can dbt alert me when a single model breaks?
Not at that granularity. dbt Cloud notifications fire on job run outcomes, and the documented triggers are succeeds, warns, fails and is canceled, so a job containing hundreds of models produces one message about the job. Recipients configure their own notifications per user rather than per job, Slack integrations support public channels only, and PagerDuty is reached through an external email address. Mapping a failure to an owner and a specific table is work that happens after the alert arrives.
Do I need a data observability tool if I already use dbt?
Only when one of two things becomes true. You need history, meaning questions about whether something is getting worse or how often it happens, in which case a dbt native package such as Elementary may be enough. Or your warehouse holds data dbt did not produce, such as raw ingestion, reverse ETL output or another team tables, and you want the tables nobody instrumented watched too. dbt sees its own DAG, and incidents are frequently upstream of the first model.
What do dbt tests not catch?
Anything nobody thought to assert, and anything that happens while dbt is not running. dbt tests check rules you wrote, against the models you chose, at the moment dbt runs them. Data observability watches every table continuously for breaks nobody predicted: a load that never arrived, a volume that halved, a column that changed type, a distribution that shifted. Tests are precise and need to be written; observability is broad and needs to be connected. Most teams that run both keep business rules in dbt and give outages, drift and schema changes to the monitoring layer.
Which dbt observability tool is best for dbt Core?
For a dbt Core only stack, Elementary is the usual answer, because it is Apache 2.0, installs through packages.yml, materializes artifacts and test results so history accumulates, and adds anomaly detection on top of that history. Its limits are that it requires dbt, it only sees what is in the project, and it runs when dbt runs. Choose a warehouse connected platform instead when you need coverage of tables dbt did not create or checks that continue when dbt is not executing.
More of the platform
Catch broken data before your stakeholders do
Connect your warehouse and get dbt observability live from one read-only connection. Transparent pricing, no credit card.