🛡 Quality, Testing & Ops · Chapter 4
Freshness, volume and anomaly monitoring
The worst data incidents don't throw errors. The job succeeds, the table is valid — and it simply has 40% fewer rows than it should. Only monitoring that knows what ‘normal’ looks like can see that.
Study this lesson free in the app →
The basics
Data monitoring is continuously watching a table's vital signs — how fresh it is, how many rows arrived, how its values are distributed — and alerting when they drift from normal.
Why it exists: tests check rules you thought of in advance (fare >= 0). But the most common real failure is silent: an upstream filter changes, one city's feed stops, a partner sends half a file. Every row is valid. The job is green. Only a comparison against history notices that Tuesday usually has 12,000 Shillong rides and today has 9,000.
The mental model — three vital signs:
- Freshness — how old is the newest data?
now() − max(event_ts)vs an SLA - Volume — did the expected amount arrive? row count vs a baseline
- Distribution — do values look like they usually do? null rate, average fare, share per city
The hard part is not computing them. It is defining normal so that alerts fire on real problems and stay quiet on Sundays and Diwali.
Words to know: SLA, baseline, seasonality, z-score, false positive.
Explained
Tuesday 4 November. Meghalaya Cabs' nightly load: ✅ succeeded. dbt tests: ✅ all 212 pass.
Shillong rides: 9,000. The last four Tuesdays: 12,100, 11,800, 12,400, 12,000.
A driver-app release had stopped sending events from older Android phones. No row was invalid. No job failed. Only a monitor that remembered last Tuesday could notice.
The milk delivery
Your milkman delivers two packets every morning at 6:30.
- 9:00 and no milk → freshness problem. Something stopped.
- Milk arrives on time but only one packet → volume problem. Something partial.
- Two packets on time, but it's toned milk instead of full cream → distribution problem. Something changed.
You notice all three without a written rule because you know what normal looks like. Monitoring is teaching the pipeline what normal looks like — including that on Sundays he comes at 7:15 and that's fine.
The three vital signs, precisely
Freshness — now() − max(event_ts). Compare with the consumer's SLA: ops dashboard 15 min, finance daily by 06:00. Also watch load freshness (max(_loaded_at)): a job can run on time and reload yesterday's file, so loaded-at is fresh while event time is stale.
Volume — row count per load or per hour. Compare to a seasonal baseline: same weekday over the last 4–8 weeks, not ‘yesterday’. Monday vs Sunday differs 30% for a cab company with no problem at all.
Distribution — per-column profiles: null %, distinct count, mean / p50 / p95 of fare, share of rides per city or payment method. A UPI share dropping from 72% to 41% overnight means something broke in payments logging, even though every row is valid.
Schema — columns added, removed or retyped. Cheap to watch, often the first sign.
A seasonal volume monitor in SQL
WITH daily AS (
SELECT city, ride_date, count(*) AS n
FROM fct_rides
WHERE ride_date >= current_date - 57
GROUP BY 1, 2
), baseline AS (
SELECT t.city, t.n AS today_n,
avg(h.n) AS mean_n,
stddev_samp(h.n) AS sd_n
FROM daily t
JOIN daily h
ON h.city = t.city
AND h.ride_date IN (t.ride_date - 7, t.ride_date - 14,
t.ride_date - 21, t.ride_date - 28) -- same weekday
WHERE t.ride_date = current_date - 1
GROUP BY 1, 2
)
SELECT city, today_n, round(mean_n) AS expected,
round((today_n - mean_n) / nullif(sd_n, 0), 1) AS z
FROM baseline
WHERE abs((today_n - mean_n) / nullif(sd_n, 0)) > 3
OR today_n < 0.7 * mean_n; -- floor for low-variance citiesShillong: mean 12,075, sd 250, today 9,000 → z ≈ −12.3. Alert.
Freshness as configuration (dbt sources)
sources:
- name: app_events
loaded_at_field: _loaded_at
freshness:
warn_after: {count: 30, period: minute}
error_after: {count: 2, period: hour}
tables:
- name: ride_completed
- name: driver_location
freshness: # tighter for the live map
warn_after: {count: 5, period: minute}
error_after: {count: 15, period: minute}dbt source freshness computes max(_loaded_at) per table and compares. Run it before dbt build so stale inputs stop the run instead of rebuilding marts on old data.
Note it checks load time. Pair it with an event-time check if replays of old files are possible.
From metric to alert without the noise
Most alert fatigue comes from skipping the baseline and context steps — comparing to yesterday and ignoring the calendar.
Diwali night, 9:00 PM
Rides in Guwahati at 21:00 are 2.4× the usual Friday. The volume monitor fires z = +9. On-call gets paged.
Nothing is broken. It's Diwali.
The fix is not to raise the threshold for everyone forever. It's:
- A calendar table of festivals and events (Diwali, Bihu, Shad Suk Mynsiem, IPL finals) that the monitor reads, downgrading spikes to an annotation.
- Asymmetric thresholds: drops are far more often bugs than spikes are. Page on z < −4; only notify on z > +6.
- Keep freshness paging as normal — Diwali doesn't excuse a late feed.
Gotchas
- Comparing to yesterday. Monday vs Sunday looks like an incident every week. Use same-weekday baselines.
- Only monitoring the total. Tura's feed dying is −3% of the company total and invisible; per-city it's −100%.
- Trusting load time as freshness. A job that reloads an old file on schedule looks perfectly fresh.
- Baselines that learn the incident. A rolling window that includes last week's outage now thinks low is normal. Exclude flagged days from the baseline.
- Z-score on tiny counts. Jowai has 40 rides on a Tuesday; ±10 is noise. Add an absolute floor or use a percentage rule for small segments.
- Alerts with no action. If the responder can only say “hm, interesting”, it should be a dashboard, not a page.
Words to know
| Freshness SLA | The maximum acceptable age of the newest data for a given consumer. |
|---|---|
| Load vs event freshness | When data arrived in the warehouse vs how recent the events in it are. They can disagree. |
| Seasonal baseline | Expected value computed from comparable periods — same weekday and hour — rather than the previous period. |
| z-score | (value − mean) / standard deviation. How many standard deviations from normal; |z| > 3 is a common alert line. |
| Segmentation | Monitoring a metric per city, partner or platform so a local failure isn't hidden in the total. |
| False positive | An alert that fires when nothing is wrong. Too many and people mute the channel. |
In two minutes
- Tests catch rule violations you predicted; monitoring catches drift from normal you didn't predict.
- Freshness compares the newest timestamp with now, against an SLA set by the consumer.
- Distinguish load freshness (when it arrived) from event freshness (how recent the events are) — a job can reload stale data on time.
- Volume compares today's row count with a baseline that respects seasonality: same weekday, same hour.
- Distribution monitors catch silent semantic changes: null rate, mean, share by category.
- Static thresholds are simple but noisy; rolling z-scores or seasonal baselines adapt to growth and weekly patterns.
- Monitor per segment (city, partner), not only in total — a dead city can hide inside a healthy total.
- Every alert needs an owner and a tolerable false-positive rate, or it will be muted within a month.
Test yourself
1. Which of these is a freshness check?
- now() − max(event_ts) compared with a 30-minute SLA
- count(*) vs the 4-week same-weekday average
- null rate of driver_id
- count(distinct ride_id) = count(*)
Show the answer
now() − max(event_ts) compared with a 30-minute SLA — Freshness is how old the newest data is. Row counts are volume, null rate is distribution, the last is uniqueness.
2. Why compare Tuesday's ride count to previous Tuesdays rather than to Monday?
- Mondays are always missing data
- Weekly seasonality — weekdays differ systematically, so day-over-day comparisons create false alerts
- SQL can't compare adjacent days
- It's cheaper to compute
Show the answer
Weekly seasonality — weekdays differ systematically, so day-over-day comparisons create false alerts — Ride demand has a strong weekly pattern. Same-weekday baselines remove it, so a deviation means something actually changed.
3. Last four Tuesdays: 12,100 / 11,800 / 12,400 / 12,000 rides (mean 12,075, sd 250). Today: 9,000. Approximate z-score?
- −1.2
- −3.1
- −12.3
- −30.8
Show the answer
−12.3 — (9,000 − 12,075) / 250 = −3,075 / 250 ≈ −12.3. Far beyond any sensible threshold — this is a real drop.
4. Company-wide rides are down 3%. Tura's feed has stopped completely. What catches it?
- A company-wide volume monitor with z > 3
- dbt source freshness on the whole table
- A uniqueness test
- A per-city volume monitor
Show the answer
A per-city volume monitor — Tura is a small share of the total, so the total barely moves. Per-segment monitoring shows Tura at −100%. Table-level freshness also stays green because other cities keep loading.
5. The UPI share of payments drops from 72% to 41% overnight; total payments and row counts are normal. What kind of monitor catches this?
- Freshness
- Volume
- Distribution
- Schema
Show the answer
Distribution — Counts and timing are fine; the mix of values changed. That's a distribution monitor — and it usually points to a logging or mapping bug, since riders don't switch payment methods overnight.
6. Trap: the nightly job reloads a file on schedule, and dbt source freshness (loaded_at_field: loadedat) is green. Can the data still be stale?
- No — green freshness means fresh data
- Yes — the file loaded today may contain old events; load time ≠ event time
- Only if the job failed
- Only for streaming sources
Show the answer
Yes — the file loaded today may contain old events; load time ≠ event time — Loaded-at measures arrival. If the upstream re-sent yesterday's export, it arrives fresh and is full of stale events. Check max(event_ts) as well.
There are 8 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.
Start this lesson →