Alert fatigue, a silent column rename, and a build blocked by freshness
Meridian Health's claims dbt project declares 22 sources and 340 data tests. Six sources have freshness checks with warn_after: {count: 12, period: hour} and error_after: {count: 24, period: hour}. The #data-alerts channel muted the dbt bot after 400 notifications in one week, roughly 85% of them the same three not_null failures on a table with a known 60-row gap. Last month the upstream eligibility feed renamed payer_id to payer_identifier and added a new nullable payer_id; nothing failed, payer_id silently returned 100% nulls, and the executive dashboard that filters on payer went blank nine days later. Separately, the nightly dbt build aborts about 30% of the way through whenever the freshness check on a rarely used marketing source fails, blocking 300 downstream models from rebuilding.
1. Design a severity and freshness policy for this project. What specifically changes about source tiers, `severity`, `warn_if`/`error_if`, and where freshness runs?
Read the worked answer
Alert volume is a routing problem before it is a test problem. Split it into three decisions.
- Source tiers. Tier 1 (claims, eligibility - feeds revenue and the executive dashboard):
warn_after6h,error_after12h. Tier 2 (operational reference data): warn 24h, error 48h. Tier 3 (marketing and other rarely refreshed extracts): no freshness configuration at all; a stale marketing CSV is not an incident. - Test severity. Reserve
severity: errorfor tests whose failure makes a downstream number wrong: uniqueness on primary keys,not_nullon keys and amounts,relationshipsto dimensions. Everything exploratory starts atseverity: warnwithstore_failures: trueinto a dedicated audit schema, so the team can query the offending rows instead of paging a human. - Volume thresholds. Noisy tests get
warn_if: '>0'anderror_if: '>500', turning a binary alarm into a signal: a 60-row gap is recorded, a 60,000-row gap fails the job.
Decouple freshness from model builds. Run dbt source freshness as its own job or job step so a stale marketing feed cannot abort a dbt build partway and strand 300 models. Then a freshness failure alerts the source owner, while a build failure means transformation logic broke - two different audiences, two different pages.
Finally, prune. A test nobody has acted on in a quarter is documentation, not a control. Demote it to warn, review warn results weekly from run_results.json, and delete the ones that never fire.
2. The rename of `payer_id` produced no test failure at all. Why did the existing suite miss it, and which checks would have caught it within a day?
Read the worked answer
Every existing test was behavioural, and this failure was structural. not_null tests ran on specific column names; when the feed renamed payer_id to payer_identifier and re-added a nullable payer_id, the source YAML still declared tests against a column that, from the model's point of view, kept existing - it just became empty. Nothing in the suite asked whether the column still carried meaning, only whether it was present.
The cheapest checks that would have caught it:
not_nullplusaccepted_valuesorrelationshipson every key and foreign key column in the source YAML, not only in marts. Anot_nullonpayer_idat the source turns 100% nulls into a failure on day one. Review that column list whenever the source team ships a release.- A schema-drift singular test querying
information_schema.columnsfor each critical source table and failing when the column set changes. This is the only check that catches a rename before a human notices the dashboard. - A distribution guard on low-cardinality columns:
not_nullcombined with a row-count threshold would have fired immediately for a column that went from 100% populated to 0%. dbt source freshnesswith a load-timeloaded_at_field, which catches a feed that stops rather than one that changes shape - necessary but not sufficient here.
Nine days is a detection-latency problem. The fix is a check on shape plus a check on occupancy, not more value-level tests on unrelated columns.
3. Three `not_null` tests fail every night on a 60-row gap in a large claims table. What are the defensible ways to handle it, and what should be avoided?
Read the worked answer
Three permanently failing tests are a tax: they train everyone to ignore the channel and they hide the next real failure. Handle the gap explicitly instead of muting it.
Option A - quarantine if the cause is known and defensible. Filter the bad rows out in the staging model, materialize them in stg_claims_rejected, and test the exception instead: a singular test that fails when the excluded count exceeds, say, 100 rows in a day. The main model is now clean, its not_null can be promoted to severity: error, and a worsening upstream problem still pages someone because the count test fires.
Option B - severity plus stored results if the cause is still under investigation. Keep the rows, set severity: warn, and add store_failures: true with a view materialization in a dedicated audit schema. Run the nightly job with --store-failures so the failing rows are queryable and the data owner can self-serve. Pair it with warn_if: '>0' and error_if: '>1000' so the test escalates when the gap grows from 60 rows to 6,000.
Choose A when the exclusion is business-justified and the real fix belongs upstream; choose B while the root cause is open. What to avoid: enabled: false, which deletes both the signal and the memory of why it went away, and blanket severity: warn on key columns, which is how the payer_id failure went unnoticed. Either path should carry a meta note with an owner and a ticket link, revisited at the next quarterly test audit.