Roadmap To Be A Data Engineer / Lesson 06
The Flatline Weekend
A dashboard goes flat and nobody notices. Tests, freshness and alert fatigue.
1The scene
Friday 28 Aug, 19:40 CEST. The hourly loader for fct_order_item picked up every order
placed before 19:00 — 181 rows for the day — and finished green. Then it stopped. An
upstream credential rotated at 19:52; the next attempt exited before it logged anything the
alerting rule recognised. No red DAG, no page.
Between Friday evening and Sunday midnight the marketplace sold 682 items worth €29,236. The warehouse learned about none of them.
Loud failure: the job runs and throws — red DAG, stack trace, Slack message. Silent failure: the job doesn’t run, or runs and writes wrong data — a dashboard that renders beautifully.
2Why nobody saw it
The trend chart didn’t draw a gap. A gap needs rows missing from a range the chart knows about. With no rows after Friday 18:59 the date axis simply ended early, and the final bar was short — which is exactly what the newest bar always looks like when today isn’t over.
The KPI card was wrong in a plausible direction.
| value | |
|---|---|
| Card said (28d GMV vs prior 28d) | −10.9% (€283,819 vs €318,558) |
| Truth | −1.7% (€313,056) |
| Hidden from the card | €29,236 — 9.3% of the window |
The uncomfortable part: −10.9% was outside all 147 comparable historical windows (observed range −5.2% to +6.2%). The anomaly was not subtle and took ~8 lines of SQL to express. It went unnoticed because nobody and nothing was computing the comparison. A human reading “−10.9%” cannot tell a soft month from a broken pipe; a machine with 147 prior windows can.
Detected Monday 11:05 by Finance, reconciling payouts against the payment provider. Time to detection: 63h 25m. With an hourly freshness check at a 4.5 h threshold it would have fired Sat 01:00 — 5h 20m. The backfill itself took 11 minutes (the loader was idempotent — Lesson 04). The repair was never the expensive part; the three days of not knowing were.
3The six tests (run for real against the broken warehouse)
| # | Test | Catches | Result |
|---|---|---|---|
| 1 | Freshness — age of max(loaded_at) |
the pipe stopped | 61.33 h stale → FAIL |
| 2 | Volume anomaly — rows vs recent distribution | partial / missing loads | 0 rows on 29 Aug vs median 242 → FAIL |
| 3 | Uniqueness — one row per key | double-counting | 3 duplicate keys, 3 extra rows → FAIL |
| 4 | Completeness — null rate, not existence | upstream behaviour change | 0.00% → 4.2–4.5% from 20 Aug → WARN |
| 5 | Referential integrity — FK exists in dim | facts pointing at nothing | 14 orphan rows, 2 brands, €755.84 at risk → FAIL |
| 6 | Accepted values / ranges | nonsense that still parses | 3 items priced €0.00 → FAIL |
-- 1. Freshness — the only test that fires when NOTHING happens
SELECT max(loaded_at) AS last_load,
date_diff('minute', max(loaded_at), now()) / 60.0 AS age_hours,
CASE WHEN date_diff('minute', max(loaded_at), now()) / 60.0 > 4.5
THEN 'FAIL' ELSE 'PASS' END AS status
FROM fct_order_item;
-- 2026-08-28 19:40:00 | 61.33 | FAIL
-- 2. Volume — note the coalesce. Written as a plain WHERE day = '2026-08-29'
-- this returns zero rows on the day it matters, and a test returning no rows PASSES.
WITH d AS (SELECT ordered_at::DATE AS day, count(*) AS rows_loaded
FROM fct_order_item GROUP BY 1),
b AS (SELECT median(rows_loaded) AS med, min(rows_loaded) AS lo FROM d
WHERE day BETWEEN DATE '2026-07-31' AND DATE '2026-08-27')
SELECT coalesce((SELECT rows_loaded FROM d WHERE day = DATE '2026-08-29'), 0) AS rows_loaded,
b.med, b.lo,
CASE WHEN coalesce((SELECT rows_loaded FROM d WHERE day = DATE '2026-08-29'), 0)
< 0.6 * b.med THEN 'FAIL' ELSE 'PASS' END AS status
FROM b;
-- 0 | 242 | 197 | FAIL
-- 3. Uniqueness -- 5. Referential integrity
SELECT count(*) AS duplicate_keys, sum(n) - count(*) AS extra_rows
FROM (SELECT order_item_id, count(*) n FROM fct_order_item GROUP BY 1 HAVING count(*) > 1);
SELECT count(*) AS orphan_rows, count(DISTINCT f.brand_id) AS missing_brands,
round(sum(f.price_cents)/100.0, 2) AS gmv_at_risk
FROM fct_order_item f LEFT JOIN dim_brand b USING (brand_id) WHERE b.brand_id IS NULL;
-- 6. Accepted values
SELECT count(*) FILTER (WHERE price_cents <= 0) AS non_positive_price,
count(*) FILTER (WHERE price_cents > 500000) AS absurd_price,
count(*) FILTER (WHERE condition_grade NOT IN ('A','B','C','D')) AS bad_grade
FROM fct_order_item;
Test 4 is the subtle one: a column that is 0.0% null for months and then 4.3% null forever
after is not a data problem, it is a product release nobody told you about (a new guest
checkout stopped sending buyer_country on 20 Aug). The total row count never moved, so
no volume test could see it — only a rate could.
4Where the tests run: Write · Audit · Publish
Load into a staging table nobody can query → run the tests there → only swap a passing batch into the table dashboards read. A failing batch is quarantined and yesterday’s good data stays published.
- Blocking, at the gate — tests 3, 5, 6. Cheap, unambiguous; a failure means this batch is wrong. Stale-but-correct beats fresh-and-wrong for almost every business question.
- Non-blocking, after publish — test 4. Describes reality rather than corruption. Publish, then tell someone it changed.
- Independent, on a clock — tests 1, 2. These must run whether or not a pipeline ran, so they cannot live inside the pipeline. This is the distinction that cost 63 hours.
5Setting the freshness threshold
Loader runs hourly at :40, with known quiet windows (nightly backup skips 02:40; longer Sunday vacuum; one monthly maintenance slot). Longest legitimate gap: 4h 20m.
| Threshold | Alert days / 30 healthy days | First alert on this outage | Hours to detection |
|---|---|---|---|
| 1.0 h | 30 (every night, all noise) | Fri 21:00 | 1.33 |
| 2.5 h | 1 | Fri 23:00 | 3.33 |
| 3.5 h | 1 | Sat 00:00 | 4.33 |
| 4.5 h | 0 | Sat 01:00 | 5.33 |
| 6.0 h | 0 | Sat 02:00 | 6.33 |
| no check | – | Mon 11:05 | 63.42 |
threshold = expected interval + longest legitimate gap + one interval of grace
The proportions are the lesson. Going from no check to a crude one recovers 58 hours. Tuning that check from 4.5 h to 2.5 h recovers 2 more. Teams routinely spend a week arguing the second number and never ship the first.
Severity
- Page — money is wrong now (duplicate keys in a payout table; freshness breach on a pricing feed). Bar: someone acts on this data before the next working hour.
- Ticket — something drifted (null rate stepped, two orphan brands). Nothing improves by fixing it at 03:00.
- Dashboard badge — a freshness stamp on the report itself: “data through Fri 28 Aug, 19:00”. Cheapest fix in the lesson; on its own it ends this incident at 09:00 Monday.
An alert nobody can act on is not a safety measure, it is a tax.
6Ask your team
- What is the oldest our most-used table may get before someone is told — and who is told?
- If a scheduled load never starts, what fires? (“The DAG would go red” is wrong — a job that doesn’t start has no colour.)
- Do our tests block the publish, or describe it afterwards?
- How many alerts did this pipeline send last month, and how many led to an action?
- Does the dashboard say when its data is from?
7Hands on (~40 min)
The dataset is fully deterministic — no random seed — so your figures match to the cent.
Generator is in the artifact (seed.py); it writes fct_order_item.csv (46,659 rows, weekend
already missing) and dim_brand.csv.
- Load both into DuckDB (
pip install duckdb,read_csv_auto). No server, no Docker. - Write the six tests. Wrap them in a function taking (name, SQL, pass condition) that prints PASS/FAIL. That function is the entire concept of a testing framework.
- Check against: 61.33 h stale · 0 rows on 29 Aug vs median 242 · 3 duplicate keys · 91 null countries · 14 orphan rows worth €755.84 · 3 items at €0.00.
- Make it a gate: load to
stg_order_item, run the blocking tests, and only thenCREATE OR REPLACE TABLE fct_order_item AS SELECT * FROM stg_order_item. Break something deliberately and watch the publish refuse. - Plot your own alert curve: list every expected load time in a week including maintenance, find the longest legitimate gap, set the threshold just above it. That number is an SLA.
Push as de-practice/06-data-quality with a README stating your freshness threshold and why.
8Takeaway
A rendering pipeline and a data pipeline fail independently. The chart drew perfectly for three days on top of a table that had stopped moving, because drawing is not knowing. Make the data assert something about itself, on a clock, whether or not anyone is looking.
Smallest useful action: put max(loaded_at) on the face of your three most-used dashboards.
9Vocabulary
- Freshness — age of the newest row. The only quality signal that fires when nothing happens.
- Silent failure — a pipeline that succeeds at producing wrong, missing or stale data.
- Write–Audit–Publish (WAP) — stage, test, then swap into the table readers query.
- Blocking vs non-blocking test — corruption blocks the publish; drift annotates it.
- Volume anomaly test — row count vs the recent distribution, not a hard-coded number.
- Referential integrity — every FK in a fact exists in its dimension.
- Alert fatigue — the state where a monitor’s output is reliably ignored.
- Time to detection — outage start → a human knows. Here: 63h25m → 5h20m.
- Data SLA — a written freshness/completeness promise for a table, with a named owner.