Roadmap To Be A Data Engineer / Lesson 25
The Schedule of Exclusions
Jobs at 99.6% uptime while one answer in eight is wrong.
1The renewal meeting
Second week of July. Slide four is one number in a green box: 99.6348% of scheduled tasks finished successfully last quarter, against an objective of 99.5%. Third green quarter running.
You ask the other question: of the numbers this company looked at last quarter and acted on, how many were right? Nobody has ever computed it. The green number is a ratio over 29,302 job runs, and nobody in the business reads a job run.
Over the same 91 days there were 47,278 reads — a person opening a dashboard, a scheduled report landing in an inbox, the repricing service pulling the markdown list at 04:00 — against 24 served objects. 6,232 of them returned an answer more than 1% away from the truth.
| Population | Service level |
|---|---|
| Job runs that finished green — the reported figure | 99.6348% |
| Answers, counting only failures a monitor can see | 99.0016% |
| Answers, counting every failure | 86.8184% |
| Tier-1 answers (the 7 objects decisions are made on) | 79.1899% |
All four are correct ratios. They disagree because they are ratios over four different populations. The second one is the tell: it is what the quarter would have scored if the only things that could go wrong were the things the monitoring stack is built to see. The instrumented world was healthy — by selection.
2An SLI is a ratio. Name the population.
- SLI — “a carefully defined quantitative measure of some aspect of the level of service that is provided.”
- SLO — “a target value or range of values for a service level that is measured by an SLI.”
- Error budget — “the difference between these two numbers is the ‘budget’ of how much ‘unreliability’ is remaining.” (All three, Google SRE.)
A ratio has a denominator, and choosing it is the whole design. The team chose job runs because the orchestrator already emits them: no new instrumentation, no judgement, no second opinion. It is free. That is exactly why it was chosen, and exactly why it measures the wrong thing.
Correlation between daily availability and daily correctness across the quarter: r = +0.0648, and 0 of the ten worst days for availability is among the ten worst for correctness. A task that fails at 03:00 and succeeds on retry at 03:12 costs the availability ratio a whole run and costs the 09:00 reader nothing.
What the objective actually allows. 0.5% of 47,278 reads is 236.39 wrong answers for the quarter — 2.60 a day for the whole company. At 99.9% you are allowed 0.52 a day. Nobody who has said “let’s aim for three nines on data quality” has multiplied it out. Google: “don’t pick a target based on current performance.”
The threshold nobody wrote down. “Materially wrong” = off by more than 1%. That was a choice, and it sets the score:
| Materially wrong means off by more than… | Wrong answers | Correctness SLI | Budget spent |
|---|---|---|---|
| 0.1% | 6,251 | 86.7782% | 26.4× |
| 0.5% | 6,232 | 86.8184% | 26.4× |
| 1% | 6,232 | 86.8184% | 26.4× |
| 2% | 4,920 | 89.5935% | 20.8× |
| 5% | 4,196 | 91.1248% | 17.8× |
| 10% | 4,013 | 91.5119% | 17.0× |
| 25% | 2,288 | 95.1605% | 9.7× |
| 50% | 275 | 99.4183% | 1.2× |
The curve is flat below 1.18% because the smallest error in the catalogue is Lesson 24’s extract. Set the line at 2% and that event stops counting on 306 days a year and counts only on Mondays. One event — the frozen currency feed, 0.4% wrong for 13 h — paged, was fixed the same day, and under the SLO’s own definition was never a bad answer at all. Lesson 20’s plural failure in a new hat.
The same ambiguity is already inside the availability figure: counting tasks after retries the quarter scored 99.6348%; counting attempts, 98.0455% — four times the failure rate from the same log. And 28 of the 91 days were individually below 99.5% in a quarter that was comfortably green.
3The schedule of exclusions
Every data-observability product sells the same four perils, because they are the four things a tool can check without knowing your business: exit status, freshness, row-count volume, declared tests.
Each event’s signature below is what the earlier lesson on it actually measured (Lesson 22: every test passes by construction; Lesson 24: freshness green 358/358; Lesson 21: 0 of 43 tests failing). Coverage is therefore computed, not asserted.
| Event | Axis | Exit | Fresh | Vol | Test | Found after | Wrong answers |
|---|---|---|---|---|---|---|---|
| PERILS ADMITTED | |||||||
| Partner intake feed never arrived (I01, L06) | event | ● | ● | ● | · | 0.25 h | 7 |
| Upstream skipped; mart not rebuilt (I02, L07) | event | · | ● | · | · | 1.20 h | 155 |
| Source dropped a column (parse error) (I03, L08) | event | ● | ● | · | · | 0.12 h | 12 |
| New condition-grade enum value (I04, L08) | event | · | · | · | ● | 2.60 h | 51 |
| Duplicate keys after a bad merge (I05, L15) | event | · | · | ● | ● | 2.90 h | 70 |
| Warehouse quota exhausted at 09:00 (I06, L10) | event | ● | ● | · | · | 0.08 h | 30 |
| not_null flood on seller_id (I07, L08) | event | · | · | · | ● | 2.70 h | 28 |
| Image CDN log gap, zero rows (I08, L06) | event | · | ● | ● | · | 0.35 h | 19 |
| Skewed join: nightly build 4h12 late (I09, L18) | extremal | · | ● | · | · | 0.30 h | — |
| Replica lag; ingest read a stale snapshot (I10, L12) | event | · | ● | ● | · | 0.45 h | 28 |
| relationships test: orphan brand keys (I11, L15) | event | · | · | · | ● | 2.80 h | 40 |
| Export task failed; Sheet not refreshed (I12, L13) | event | ● | ● | · | · | 0.20 h | 16 |
| Currency feed stale; FX frozen (I13, L06) | event | · | ● | · | · | 1.90 h | — |
| accepted_values: new payout status (I14, L08) | event | · | · | · | ● | 2.75 h | 16 |
| EXCLUDED FROM COVER | |||||||
| BI extract one day behind, by schedule (I15, L24) | detached | · | · | · | · | never | 1,643 |
| Watermark silently drops intake rows (I16, L22) | absent | · | · | · | · | 19.0 d | 822 |
| One person, several customer rows (I17, L19) | referential | · | · | · | · | 22.5 d | 507 |
| condition_grade_raw quietly recoded (I18, L21) | remote | · | · | · | · | 17.0 d | 555 |
| Polling CDC never sees a delete (I19, L12) | drift | · | · | · | · | 14.5 d | 670 |
| Cleaning rule applied twice, differently (I20, L11) | constant | · | · | · | · | never | 951 |
| Nightly rebuild restores erased subjects (I21, L14) | reversible | · | · | · | · | never | 206 |
| As-of join lost; the CI seed hides it (I22, L15) | unreproduced | · | · | · | · | 9.5 d | 468 |
| Non-atomic rewrite window, 9.67 s a night (I23, L17) | transient | · | · | · | · | never | 1 |
| NOT REPRESENTABLE | |||||||
| Three live definitions of GMV (I24, L20) | plural | · | · | · | · | never | — |
| Masked columns, unmasked join key (I25, L23) | permitted | · | · | · | · | never | — |
| 75.9% of the warehouse bill is one tile (I26, L10/16) | bundled | · | · | · | · | never | — |
14 of 26 events trip a peril — 53.85%. Score the same schedule by the thing the business suffers and it admits 7.57% of the wrong answers.
The four perils detect events — something changed, suddenly, visibly in a count or a timestamp. Twelve of twenty-six are not that shape. A constant is not an event. A drift is not an event. A row that never arrives leaves no trace among the rows that did. An extract a day behind by design has a perfectly fresh timestamp of its own. Three served objects — the exec weekly GMV tile, the Monday trading pack and sell-through by cohort — were materially wrong on every single read of the quarter, with no alert on any of them.
The cleanest catch of the 91 days points the same way from the other side: the skewed join that ran 4 h 12 late tripped freshness at 05:18, paged, and was diagnosed by breakfast. It produced zero wrong answers.
“You stacked the deck”
Each excluded event alone, with the other twenty-five deleted:
| If this were the only excluded event | Axis | Wrong answers | SLI | Budget | 99.5%? |
|---|---|---|---|---|---|
| BI extract one day behind, by schedule (I15, L24) | detached | 1,643 | 96.5248% | 7.0× | missed |
| Watermark silently drops intake rows (I16, L22) | absent | 822 | 98.2613% | 3.5× | missed |
| One person, several customer rows (I17, L19) | referential | 507 | 98.9276% | 2.1× | missed |
| condition_grade_raw quietly recoded (I18, L21) | remote | 555 | 98.8261% | 2.3× | missed |
| Polling CDC never sees a delete (I19, L12) | drift | 670 | 98.5829% | 2.8× | missed |
| Cleaning rule applied twice, differently (I20, L11) | constant | 951 | 97.9885% | 4.0× | missed |
| Nightly rebuild restores erased subjects (I21, L14) | reversible | 206 | 99.5643% | 0.9× | met |
| As-of join lost; the CI seed hides it (I22, L15) | unreproduced | 468 | 99.0101% | 2.0× | missed |
| Non-atomic rewrite window, 9.67 s a night (I23, L17) | transient | 1 | 99.9979% | 0.0× | met |
Seven of nine miss a 99.5% correctness objective on their own.
4The claim is the duration, not the event
Cost = read rate × duration, and duration splits into before-anyone-knew and after. Of the 6,232 wrong answers, 5,521 (88.59%) were served before anybody knew there was anything to fix.
| Mean time to detection when a peril trips | 1.33 h |
| Mean time to detection when a person notices instead | 16.5 days |
| Ratio | 298× |
| Events still running when the quarter closed | 7 |
Every one of the nine excluded events burns the correctness budget fast enough that Google’s published burn-rate rule (14.4× over 1 h, 6× over 6 h) would have paged within hours of onset — seven of the nine in under seven hours, against actual detection of 9–22 days or never. Nothing was missing from the alerting design. What was missing is a column saying whether an answer was right.
The counterfactual ladder
| Regime | Wrong answers | vs. baseline |
|---|---|---|
| What actually happened | 6,232 | — |
| Halve every repair time (MTTR ÷ 2) | 5,901 | −5.31% |
| Halve every detection time (MTTD ÷ 2) | 4,898 | −21.41% |
| Nothing goes unseen for a day | 1,075 | −82.75% |
| Nothing goes unseen for an hour | 853 | −86.31% |
Halving MTTD buys only 21.41% because the expensive events have no detection time to halve. The prize is going from never to daily, not from daily to hourly (worth a further 3.56%).
5The excess, and the rate at which it is spent
An error budget makes reliability negotiable instead of moral: you are not asking anyone to be careful, you are handing them an allowance.
- Availability budget: 146.51 runs → ended the quarter 73.03% spent.
- Correctness budget: 236.39 answers → gone by 09:00 on 3 April, day 3 of 91, and closed the quarter 26.4× over.
Burn rate = how fast the budget is going relative to the objective. Google’s published configuration for a 99.9% SLO (SRE Workbook, table 5-8, read from source):
| Severity | Long window | Short window | Burn rate | Budget consumed |
|---|---|---|---|---|
| page | 1 hour | 5 minutes | 14.4 | 2% |
| page | 6 hours | 30 minutes | 6 | 5% |
| ticket | 3 days | 6 hours | 1 | 10% |
One honest limit. Switch the correctness SLI on tomorrow and the 6 h / 6× rule fires on 84.8% of the hours in the quarter (median burn 22.2×). A burn-rate alert on a system permanently outside its objective is a lamp that is always on — which is the SLO doing its job, telling you the objective is wrong for the scope.
6What actually paged
248 pages in 91 days — 2.73 a day — of which 22 were a real event: 8.87% precision and 49.6 hours of triage at 12 min each (the one assumption in this lesson).
| Channel | Pages | Real | Precision |
|---|---|---|---|
| Task exit status | 107 | 4 | 3.7% |
| Row-count volume band | 89 | 4 | 4.5% |
| Freshness threshold | 47 | 9 | 19.1% |
| Declared tests, severity error | 5 | 5 | 100.0% |
| Declared tests, severity warn | 1,147 log lines | — | not read |
The best-precision channel in the stack is the smallest, and it sits next to 1,147 warn lines nobody has read since week two (Lesson 15 measured a real failure arriving into that stream at z = +0.44).
Route by consequence, not by channel. Page only on a wrong answer in a tier-1 object:
| as configured | routed | |
|---|---|---|
| Pages a quarter | 248 | 13 |
| Precision | 8.87% | 46.15% |
| Triage | 49.6 h | 2.6 h |
Three tiers, written down, is the whole on-call design: page (a tier-1 answer is wrong now) · ticket (wrong, not in front of a decision today, named owner + due date) · dashboard (everything else, reviewed weekly, never routed to a phone).
7There is no SLI without a second opinion
An availability SLI is free, because the system emits its own exit codes. A correctness SLI is never free, because being right is not a property a system can observe about itself. To know whether an answer was right you need a second path to the same fact, and a second path costs money to build, run and keep honest.
That is why data SLOs are hard — and it is not what the vendor slides say. The objective is easy. The budget is easy. The alerting policy is solved, published and free to copy. The indicator is the whole problem. Which makes it a budgeting question, and therefore answerable. Four second opinions this series has already built and measured:
| Check | What it asks | Catches | Detects within | Per year |
|---|---|---|---|---|
| C1 Served-window freshness (L24) | served-window freshness: max(order_date) in the copy >= last day claimed | I15 | 24 h | $0.022 |
| C2 Two-sided row count (L22) | daily two-sided row count, source system vs warehouse | I16, I19 | 24 h | $0.292 |
| C3 Rolling-minimum stale share (L13) | 30-day rolling minimum of the stale share in the destination | I21 | 31 days | $0.653 |
| C4 Column-level lineage gate (L21) | column-level lineage gate in CI – fires before the merge | I18, I22 | before merge | $0.000 |
| The whole portfolio | 6 of 26 | ≤ 1 day, mostly | $0.97 |
None of these knows the truth. Each asks a structural yes/no question — no threshold, no ground truth.
| before | after | |
|---|---|---|
| Correctness, all served objects | 86.8184% | 95.0823% |
| Correctness, tier-1 objects | 79.1899% | 98.0250% |
| Wrong answers in the quarter | 6,232 | 2,325 |
Three events survive the entire portfolio, and the residue is the honest part:
- One person, several customer rows (L19) — every row correct; the error is in an assumed correspondence stored nowhere. Needs a bridge table.
- A cleaning rule applied twice, differently (L11) — a constant, not an event; there is no before to reconcile against. Needs one cleaning layer.
- The 9.67-second rewrite window (L17) — expected cost 0.45 answers a quarter, of which one landed, on the repricing service, which wrote what it read onto live listings. Needs an atomic write.
Architecture, not instrumentation. Knowing which of your problems is which is most of what an SLO review is for.
And the sampling audit you were going to do instead. Re-computing 5 tier-1 answers a week has a 2.48% chance of catching a problem running at the rate the objective allows. Policing 99.5% by sampling needs 321 re-computations a week; at k = 5 the smallest rate you can reliably see is 27.52%. Sample to calibrate structural checks; never to replace them.
8What you actually write down
slo: tier1_answer_correctness
indicator:
population: every read of a tier-1 served object
good: the object's answer is within 1% of the same figure
recomputed from source by an independent path
measured_by: four structural checks (C1-C4), not a sample
materiality: 1.0% # changing this changes the score. owner: CTO
objective: 99.0% over a rolling 28 days
budget: 1.0% = 27.6 answers per 28 days
policy:
budget > 25% remaining: ship normally
budget < 25% remaining: no new served objects until it recovers
budget exhausted: the next sprint is reliability work, by default
alerting:
page: burn rate >= 6 over 6h, confirmed over 30m # tier-1 only
ticket: burn rate >= 1 over 3d
review: everything else, weekly, by a person
review:
every incident: how long was it wrong before anything noticed?
every quarter: which check would have caught it, and what does it cost?
Two details do most of the work. The objective is 99.0% over seven objects, not 99.5% over twenty-four — it is the only number the measured evidence supports (98.0250% with the portfolio), and an objective you miss every week is indistinguishable from no objective. The budget policy is the part with teeth — an error budget with no consequence attached is a chart; the consequence must be agreed in advance, in writing, by the person who will later want to ship instead.
The incident review question
Replace “what broke and how did we fix it” (repair is 5.31% of the problem) with:
- How long was it wrong before anything noticed? If more than a day, the finding is the gap, not the bug.
- What is the cheapest check that would have made that number smaller, and what does it cost per year? If the answer is under a dollar — four times out of six here, it was — the reason it does not exist is not budget. Nobody has been asked to name one.
9Ask the team
- “Our reliability number — what is the denominator?” Jobs, runs, tasks or pipelines means you are being shown the health of the instrument, not the service.
- “Name the five numbers we would most hate to be wrong. What checks whether they are right?” The useful answer is a query, not a person’s name.
- “Last time a number was wrong, how long was it wrong before we knew?” If nobody can say, that is the finding — start recording it in every postmortem this week.
- “How many pages did on-call get last month, and how many were real?” Under 20% precision, the rota is being trained to ignore the pager.
- “What does ‘wrong’ mean — by how much?” No materiality threshold means several undocumented ones.
10Hands on (40 minutes, no warehouse)
The generator in the artifact writes read_log.csv and job_run.csv beside itself.
- Compute both SLIs — you should get 99.6348% and 86.8184% exactly.
- Move materiality from 1% to 2% and watch 1,312 wrong answers become right ones with nothing changing in the data. Decide what you would defend, and why.
- Find the hour the budget ran out. Then set the objective to 99.9% and find it again.
- Build the burn-rate alert and count the hours it would be firing (ours: 84.8%). Ask whether the answer is a different threshold or a different scope.
- List the ten served objects your company actually decides with. For each, write the one query that would tell you it is wrong without knowing the right answer. The ones where you cannot write that query are your tier-1 risks — and you have just written your first schedule of exclusions.
Show the full code (31 lines)
-- the SLI the team reports
select count(*) as task_runs,
count(*) filter (where not succeeded) as failed,
1 - count(*) filter (where not succeeded)*1.0/count(*) as sli_availability
from job_run;
-- the SLI nobody reports
select count(*) as reads,
count(*) filter (where rel_error > 0.01) as bad,
1 - count(*) filter (where rel_error > 0.01)*1.0/count(*) as sli_correctness
from read_log;
-- the hour the quarter's error budget ran out
with hourly as (
select date_trunc('hour', read_ts) as h, count(*) as reads,
count(*) filter (where rel_error > 0.01) as bad
from read_log group by 1)
select h, sum(bad) over (order by h) as cum_bad
from hourly qualify cum_bad >= 0.005 * (select count(*) from read_log)
order by h limit 1;
-- burn rate on a six-hour sliding window, against a 99.5% objective
with hourly as (
select date_trunc('hour', read_ts) as h, count(*) as reads,
count(*) filter (where rel_error > 0.01) as bad
from read_log group by 1)
select h, sum(bad) over w * 1.0 / nullif(sum(reads) over w, 0) / 0.005 as burn_6h
from hourly
window w as (order by h rows between 5 preceding and current row)
qualify burn_6h >= 6.0
order by h;
11Takeaway
An SLI is a ratio, and whoever picks the denominator picks the answer. Pick job runs and the instrument reports on itself — 99.6348%, green, three quarters running. Pick the answers people act on and the same 91 days score 86.8184%. Availability telemetry is free because a system can observe its own exit codes; correctness telemetry is never free, because being right is not a property a system can observe about itself. Everything else — the objective, the budget, the burn-rate alerting — is solved and copyable. The error budget for data is, in practice, a budget for how much second opinion you buy: this quarter’s was $0.97 a year and would have taken tier-1 from 79.1899% to 98.0250%.
Vocabulary
| Term | What it means, and the part people get wrong |
|---|---|
| SLI | A defined quantitative measure of the service. It is a ratio; almost every mistake here is about its denominator. |
| SLO | A target value for an SLI. Not an SLA (which has money attached) and not a dashboard. |
| Error budget | 1 − SLO, multiplied out into countable units. Without a written consequence it is decoration. |
| Burn rate | How fast the budget is going relative to the objective. 1× spends the whole allowance in one window; 14.4× spends 2% of it in an hour. |
| Multiwindow, multi-burn-rate | Alerting on a long window and a short one at once: the long one decides it matters, the short one makes the alert stop when the problem does. |
| MTTD / MTTR | Time to detection and to repair. Detection was 88.59% of the damage here. You cannot halve a detection time you do not have. |
| Materiality threshold | How wrong an answer must be to count as wrong. Undocumented almost everywhere, and it sets the score. |
| Tier-1 object | A served answer somebody decides with — the right scope for a first correctness SLO, because it is the scope you can afford a second opinion on. |
| Error budget policy | What happens when the budget is spent, agreed in advance by the person who will later want to ship instead. |
| Structural check | A yes/no question about shape rather than value. No ground truth, no threshold, no tuning. |
12How these numbers were made
One deterministic generator, no random seed (splitmix64 over integer keys), simulating 91 days of 29,302 task runs and 47,278 reads against 24 served objects, scored against 26 incidents. Three things worth stating plainly:
- The signatures are not free parameters. Whether an incident trips freshness, volume, exit status or a test is taken from what the earlier lesson on it measured. Coverage in CL. 25.03 is computed from those signatures.
- The error magnitudes are the earlier lessons’ own numbers (1.18% and 16.89% from L24, 10.71% from L22, 31.17% from L21, 34.06% from L11…). What is constructed is the portfolio — all fifteen failure shapes in one quarter — which is why CL. 25.03 also reports each alone.
- The SQL printed here was run. Both logs loaded into DuckDB, published queries executed: 20 checks, all matching the Python exactly.
- Appendix reproduction check: generator extracted from the rendered HTML, run in a clean directory — 22 top-level keys, 1,521 scalar values, 0 differences, no container path in the shipped code.
One quantity is an assumption rather than a measurement and is flagged as such: 12 minutes to triage a page, used only for the 49.6-hour figure.
Series links: builds directly on L06 (the six tests, alert fatigue), L11 (constants), L12 (drifts), L13 (unwatched, and the detection ladder), L14 (reversible), L15 (unreproduced, the muted WARN), L16 (bundled), L17 (transient), L18 (extremal), L19 (referential), L20 (plural), L21 (remote, the lineage gate), L22 (absent, the two-sided count), L23 (permitted), L24 (detached, the served-window check).