◐ ai-teacher

🛡 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:

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.

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 cities

Shillong: 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:

  1. A calendar table of festivals and events (Diwali, Bihu, Shad Suk Mynsiem, IPL finals) that the monitor reads, downgrading spikes to an annotation.
  2. Asymmetric thresholds: drops are far more often bugs than spikes are. Page on z < −4; only notify on z > +6.
  3. Keep freshness paging as normal — Diwali doesn't excuse a late feed.

Gotchas

Words to know

Freshness SLAThe maximum acceptable age of the newest data for a given consumer.
Load vs event freshnessWhen data arrived in the warehouse vs how recent the events in it are. They can disagree.
Seasonal baselineExpected 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.
SegmentationMonitoring a metric per city, partner or platform so a local failure isn't hidden in the total.
False positiveAn alert that fires when nothing is wrong. Too many and people mute the channel.

In two minutes

Test yourself

1. Which of these is a freshness check?

  1. now() − max(event_ts) compared with a 30-minute SLA
  2. count(*) vs the 4-week same-weekday average
  3. null rate of driver_id
  4. 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?

  1. Mondays are always missing data
  2. Weekly seasonality — weekdays differ systematically, so day-over-day comparisons create false alerts
  3. SQL can't compare adjacent days
  4. 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. −1.2
  2. −3.1
  3. −12.3
  4. −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?

  1. A company-wide volume monitor with z > 3
  2. dbt source freshness on the whole table
  3. A uniqueness test
  4. 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?

  1. Freshness
  2. Volume
  3. Distribution
  4. 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?

  1. No — green freshness means fresh data
  2. Yes — the file loaded today may contain old events; load time ≠ event time
  3. Only if the job failed
  4. 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.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →