Debugging a Data Incident
Consumer symptom to serving dataset to transformation to upstream dataset to ingestion to source. Debug upstream, always — and diagnose from the set of checks that failed, not from the first one.
Who needs this, what one row is, and why the obvious build breaks
Every lesson starts from the consumer, because designing from the source outward is this domain's characteristic mistake.
The number is confirmed wrong. What is the sequence of questions that turns that into a cause, and in which direction do I walk?
The engineer holding the incident, who needs a procedure rather than inspiration at two in the afternoon; and every consumer waiting to be told which periods to distrust (Who Actually Consumes This Data).
One question, asked once per node on the path: is the affected period complete and correct here? The unit of the walk is a dataset-period, and the first "no" going upstream is where the incident lives.
Start at the symptom and work forwards from the beginning of the pipeline, checking each stage in order until something looks wrong. It feels systematic, it matches the direction the data flows, and it is how everyone debugs a pipeline the first time.
The pipeline has six stages and the fault is in the fifth. Working forwards you have examined four healthy stages before reaching it, each of which took a query and a judgement call.
- The pipeline has six stages and the fault is in the fifth. Working forwards you have examined four healthy stages before reaching it, each of which took a query and a judgement call.
- Several stages look mildly unusual — a slightly low count here, an odd null rate there — and forwards you have no way to tell which is the cause and which is a consequence of it (Correlation Is Not the Root Cause).
- The fault is not in the pipeline at all. It is in the BI layer, one hop past the last stage you checked, and forwards you reach it last (Where Did This Number Come From?).
- The fan-in means "the beginning" is five sources. Forwards, you must check all of them; upstream from the symptom, the transformation tells you which one the affected column came from (Column-Level Lineage).
- Half an hour in, someone re-runs a job to see if it helps. The evidence for the periods you had not yet examined is gone (What Backfills Break).
What is actually happening
- Upstream traversal works because corruption propagates downstream and only downstream. If a dataset is correct for the affected period, everything upstream of it is irrelevant to this incident, and the search space halves at every hop that passes (Data Lineage).
- It is a binary search over the lineage path, and the question at each node is the same one, which is what makes it a procedure rather than a talent. The termination condition is the first node where the answer is "no" — the boundary between a correct upstream and an incorrect downstream is the fault.
- The walk terminates in one of three places, and only two of them are your incident. The source is wrong: an application bug or a genuine business change. A hop lost, duplicated or delayed data: an ingestion or delivery fault. Or a transformation is wrong: the code did what it was told (Two Dashboards, Two Numbers).
- Which node to start from is decided by the *evidence*, not by the path. Running the full check suite over the affected period gives a failing set, and the failing set narrows the candidate causes before the first hop is examined — which is what turns a six-hop walk into a two-hop confirmation.
- The fingerprint works because the checks measure independent properties. Completeness compares identities against the source; uniqueness counts repeats; validity inspects types; distribution compares shape; freshness compares clocks; reconciliation compares totals. A fault that moves one leaves the others alone unless it genuinely touches what they measure.
Debug upstream, always
There is one direction to search and it is against the flow. Corruption travels downstream, so a dataset that is correct for the affected period exonerates everything above it, and every hop that passes removes the entire subgraph behind it from consideration.
The ladder below is the path in the direction you walk it. The question is identical at every rung — *is the affected period complete and correct here* — which is what makes it a procedure that a tired person can follow at the end of a long day rather than an exercise in intuition.
Two rungs are routinely omitted and both are near the top. The BI layer applies its own filters, joins and cached extracts, and a number can be wrong there while every dataset beneath it is perfect. And the metric definition — the semantic layer, or the piece of SQL pasted into the tile — is a transformation like any other, with no tests and no lineage (The Metrics Layer).
Start at ingestion and check each stage in order until something looks wrong. Every stage needs a query and a judgement, and consequences of the real fault appear as anomalies along the way.
Start at the tile, reproduce the number one layer down, and keep asking the same question at each upstream dataset. Stop at the first node where the affected period is complete and correct; the fault is immediately downstream of it.
Corruption propagates in one direction, so a passing node upstream eliminates everything above it while a passing node downstream eliminates nothing. Forwards, an anomaly is ambiguous between cause and consequence; upstream, the first failure you meet is the earliest one, which is by construction the cause.
WALK THIS WAY ↑ upstream
↑ Source system is the world actually like this?
↑ Ingestion / CDC did every committed change get emitted?
↑ Raw landing is the payload present, and shaped as before?
↑ Staging model did dedup / window / cast keep the period whole?
↑ Serving model does the fact table reconcile for the period?
↑ Mart / aggregate does it match the model it derives from?
↑ Metric definition does the SQL behind the tile mean what we think?
↑ BI layer does the tool add a filter, join or cached extract?
● Consumer symptom "revenue is down eighty percent"
At each rung, one question:
is the affected period complete and correct HERE?
First "no" going up → the fault is between that rung and the one below it.Diagnosis from a failing-check fingerprint
src/de/sim/pipeline.ts in this repository under one fault at a time, not measured on a production system. The fault set, the check set and the one-fault assumption are all deliberate teaching choices; what transfers is which properties move together, not the specific dataset or any magnitude.Before the first hop, run the whole check suite over the affected period and read the failures as a set. The set is far more informative than any member of it, because the checks measure independent properties and a fault only moves the ones it genuinely touches.
The table below comes from the pipeline model in this repository — eight faults injected one at a time against six checks, with a test asserting that no two faults produce the same failing set. That assertion is what makes it a diagnostic exercise: if two causes were indistinguishable from the evidence, no amount of care would separate them.
Three rows deserve attention. Broken transform logic fails reconciliation and nothing else — every row present, unique, fresh, well-typed and normally shaped, and the total wrong. Skew fails distribution and nothing else, with revenue exactly correct, because it is a performance fault that this suite correctly declines to call a data incident (Data Skew). And duplicate delivery fails uniqueness without failing completeness, because duplication is an addition, not a loss.
| Injected fault | Checks that fail | Direction of the error | What the fingerprint tells you |
|---|---|---|---|
| Change capture down for a window | completeness, distribution, reconciliation | Understated. | Completeness failing alongside reconciliation means identities that exist upstream are absent here. Walk to the connector and ask what its position was during the window (CDC Failure Modes and the Retention Deadline). |
| At-least-once redelivery | uniqueness, reconciliation | Overstated. | Uniqueness without completeness: nothing was lost, something arrived twice. The fix is deduplication, and the fault is normal broker behaviour rather than a malfunction (Deduplication). |
| Events arriving after the window closed | completeness, reconciliation | Understated, and self-correcting into the next period. | Completeness fails while distribution holds, because the loss is spread rather than concentrated. Look at arrival time against event time before suspecting ingestion (Late Events). |
| Producer sends a number as a string | validity, reconciliation | Zeroed. | Validity is the discriminator: rows are all present and all unique, and the measure is null. A cast produced null rather than raising (Breaking Schema Changes). |
| Transform stops subtracting refunds | reconciliation only | Overstated. | The domain's signature failure. Nothing else moves, so no signal except reconciliation exists — diff the model against its previous version (Two Dashboards, Two Numbers). |
| Transform job crashes, run abandoned | completeness, distribution, freshness, reconciliation | Nothing published; the table holds the previous period. | Freshness is the discriminator and it appears in only this row. Everything failing at once with freshness among them means nothing was published (Stale Dashboards). |
| One key holds most of the day | distribution only | None — the total is exactly right. | A performance fault, not a correctness one. Revenue reconciles perfectly; the job simply finishes when its slowest task does (Data Skew). |
| Backfill appends instead of replacing | distribution, uniqueness, reconciliation | Doubled. | Uniqueness plus distribution, without completeness: the period is present twice. Distinguished from redelivery by scale and by the deploy timeline (What Backfills Break). |
Localising the loss to one arrow
Once the fingerprint says rows are missing, the remaining question is *where*, and that is answered by counting at every hop for the affected period rather than by reading logs. A drop between two adjacent hops localises the fault to one arrow; a uniform count with a wrong total localises it to logic instead.
The queries below are deliberately plain. During an incident the value of a query is how quickly it can be understood by a second person, and a clever query that needs explaining costs more than it saves. Both are bounded to the affected period, which is the difference between a fast answer and an expensive one (Partition Pruning).
The second query is the one people forget: comparing the affected period against a known-good one for the same weekday. Without a control, every number looks slightly unusual under stress; with a control, "different" and "normal" separate immediately (Volume Anomalies).
1-- 1. Per-hop counts for the affected period, beside a known-good control.2-- A drop between adjacent hops localises the fault to one arrow.3select4 hop,5 count(*) filter (where logical_period = date '2026-08-24') as affected,6 count(*) filter (where logical_period = date '2026-08-17') as control_same_weekday7from (8 select 'raw' as hop, logical_period, order_id from raw_order_events9 union all10 select 'staging', logical_period, order_id from stg_orders11 union all12 select 'fact', logical_period, order_id from fct_orders13) hops14where logical_period in (date '2026-08-24', date '2026-08-17')15group by hop16order by case hop when 'raw' then 1 when 'staging' then 2 else 3 end;17 18-- 2. Which specific identities exist upstream and not downstream.19-- Reading a handful of them usually names the cause outright:20-- all from one country, all in one four-hour window, all one status.21select r.order_id, r.event_ts, r.country, r.status22from raw_order_events r23left join fct_orders f24 on f.order_id = r.order_id25 and f.logical_period = r.logical_period26where r.logical_period = date '2026-08-24'27and f.order_id is null28order by r.event_ts29limit 50;30 31-- 3. Counts equal, totals wrong: the fault is in logic, not in delivery.32select33 count(*) as rows_present,34 count(*) filter (where amount is null) as null_amounts,35 count(distinct order_id) as distinct_orders,36 sum(amount) as total37from fct_orders38where logical_period = date '2026-08-24';Query three is the discriminator that the first two cannot provide: equal row counts with a wrong total, and no nulls, means every row arrived and the logic that produced the measure is wrong. That is the case where no ingestion investigation will ever find anything.
How to build it
Most important first.
- Run the check suite over the affected period first, and read the failing set as a whole. Two minutes of evidence beats twenty minutes of hypothesis, and it tells you which hop to examine first (Data Tests).
- Then walk upstream, one dataset at a time, asking only whether the affected period is complete and correct there. Resist the urge to fix anything you notice on the way — record it and continue (Data Incidents).
- Bound every investigative query to the affected partitions. An unbounded query during an incident is slow when you most need it fast, and expensive in a way that shows up later (Partition Pruning).
- Compare against a known-good period, not against intuition. The same query over last Tuesday is the control, and having a control is the difference between "this looks odd" and "this is different".
- Preserve evidence before repairing: snapshot the affected partitions, or at minimum record the counts and check results, so a re-run cannot erase what you were looking at (Keeping Raw History: The Recovery Position and the Liability).
- Write the cause down as a fingerprint — which checks failed, at which hop, for which periods. That record is what makes the second occurrence a five-minute diagnosis (Reading a Timeline: Observation Order Is Not Causal Order).
What this actually promises
Naming the guarantee you do not have is worth more than naming the one you do — everything downstream inherits the weakest promise in the chain.
- A node that passes every check for the affected period guarantees that this incident did not originate at or above it — for the properties the checks measure. It says nothing about properties nobody checks.
- The fingerprint narrows the cause; it does not prove it. Two distinct faults with overlapping symptoms can produce the same failing set in a real platform, which is why the walk confirms the hypothesis rather than replacing it.
- Finding the hop guarantees you know where, not why. "The transform is wrong" is a location; the cause is a code diff, a schema change or a configuration change, and those are found by a different search (Deploys Are the First Suspect).
- Nothing guarantees a single cause. Incidents with two simultaneous faults are common enough that a diagnosis explaining only part of the discrepancy should be treated as incomplete rather than as done.
Can I trust it?
A green pipeline is evidence that code ran. These four fields are the evidence that the data is right.
- The check that makes the whole procedure fast: per-hop row counts and per-hop measure sums, recorded per logical period at pipeline run time. With them, the walk is one query; without them, it is one query per hop against production tables.
- It misses any fault that preserves both count and sum at a hop — a swap of values between rows, a wrong join key that matched a different but equally-sized population.
- It also misses everything about columns nobody sums. A dimension attribute that started arriving wrong changes no count and no total (Distribution Tests).
- Each hop's answer is only as good as that dataset's own completeness for the period. Examining an open period gives an answer that will change, so bound the walk to closed periods wherever the fault permits it (Late-Arriving Data).
- The walk is fast when per-hop counts already exist as data and slow when each hop requires a fresh query against a large table. Pre-computing per-hop counts is the highest-leverage preparation for an incident you have not had yet (Pipeline Metrics).
- Replay-based confirmation — recomputing a period from raw and comparing — is the strongest evidence available and the slowest. Reach for it when the cheaper hops disagree, not first (Reprocessing vs Retrying).
- When the pipeline shape changes, historical per-hop counts stop being comparable across the change. Record the pipeline version with the counts so a discontinuity during an investigation is attributable rather than alarming.
- A schema change is one of the highest-prior causes and one of the easiest to check: compare the raw payload's field set and types for the affected period against a known-good period, before examining any transformation (CDC and Schema Drift).
- Semantic changes leave no trace anywhere in the walk. Every hop is complete, unique, well-typed and normally shaped, and the number is still wrong — which is why the walk must end at a conversation with the producer rather than at a passing check (Semantic Changes).
- Repair at the hop where the fault was introduced, then recompute forward. Repairing downstream of the fault leaves the upstream wrong and guarantees the next rebuild reintroduces the problem (Model Layering).
- Establish the affected range by running the discriminating check backwards through history until it passes. This is the step that is skipped and is the difference between a repair and a partial repair (Validating a Backfill Before You Publish).
- Recompute into a location consumers are not reading, validate against the source for the range, then publish atomically. Never mutate a serving table in place during an incident (Atomic Publish).
What can go wrong
- Debugging forwards, which examines healthy stages first and reaches the BI layer last.
- Re-running a job mid-investigation, which destroys the evidence for every period you had not yet examined.
- Stopping at the first anomaly rather than the first *upstream* anomaly. A low count at hop five is usually the consequence of a low count at hop two.
- Concluding from a single failing check. Reconciliation fails for almost every cause and therefore discriminates between none of them.
- Diagnosing on an open period, where a legitimate late arrival looks exactly like a loss (Late Events).
- Finding the hop, fixing forward, and never establishing how far back the fault extends.
- "Reconciliation failed, so we lost rows." Reconciliation fails for losses, duplicates, nulled casts, abandoned runs and wrong logic. On its own it identifies nothing (Reconciliation).
- "The counts match, so the data is fine." Counts match for every value-level fault. The model in this repo has one whose entire signature is a reconciliation failure with every other check green.
- "Start at the source and work forward." Corruption only flows downstream, so a forward search examines healthy stages first. Upstream from the symptom halves the search space at every passing hop.
- "We found the broken hop, so the incident is resolved." Finding the hop is the middle of the work. The affected range and the repair are the rest, and the range is the part that is skipped (Backfills).
Operating it
- The failing-check set for the affected period, read as a set. This is the first artefact of the investigation and the one to record (The Data Quality Dashboard).
- Per-hop row counts and measure sums for the affected period beside the same numbers for a known-good period.
- Deployment and configuration changes on the same timeline as the affected periods — a large share of transformation faults have a deploy immediately before them ("What Changed?" — Deploy Markers and the Invisible Deploys).
- The raw payload's field set and types for the affected period against a known-good one, which is the fastest schema-drift test available (Schema Registry).
- At ten times the datasets, the path from symptom to source is no longer memorable and the walk requires a generated lineage graph rather than a person who knows the platform (Data Lineage).
- At a hundred times, the walk itself must be tooled: click a metric, see each upstream node with its checks for the affected period already evaluated. That is the shape of a lineage debugger, and it is a product because the manual version stops fitting in an incident (Lineage Debugging).
- Deeper pipelines make the walk longer logarithmically at best and linearly in practice, which is a genuine and under-discussed argument against gratuitous layering (Raw, Staging, Curated: Layers by Purpose).
- Investigation cost is bytes scanned times the number of times an unsure engineer re-runs the query. Bounding to the affected partitions and keeping a known-good comparison period small is the whole optimisation (Scan Cost).
- Pre-computed per-hop counts convert most of that scanning into a lookup against a tiny table. They are cheap to produce at run time because the job already has the numbers (Pipeline Metrics).
- Replay-based confirmation is the most expensive step: it recomputes a period from raw. It is worth it when it settles a disagreement between cheaper signals, and wasteful as a first move (Compute Waste).
- The evidence-first approach requires a check suite that can be run over an arbitrary past period. Building that is real work done long before the incident, and it is the difference between a procedure and a talent.
- Preserving evidence before repairing delays the repair. Consumers experience the delay and never experience the corrupted evidence that would have followed, so the trade is unpopular and correct.
- Per-hop instrumentation makes incidents dramatically faster and adds code to every pipeline. Written once as a shared wrapper it is cheap; copied into two hundred models it is a maintenance problem (Pipeline Observability).
Data incident simulator
Change an input and watch which number moves — and which one does not. Everything here comes from a model in this repository, not from a measurement.
Finance says yesterday's revenue is far below the rest of the week. Nothing was deployed.
Where this applies
Almost nothing here is universal. These labels say what each claim is specific to, and where a different engine, format, warehouse or scale would differ.
- GENERALUpstream traversal follows from corruption propagating in one direction only, which is a property of dataflow rather than of any tool. It applies equally to a batch DAG, a streaming topology and a chain of hand-written scripts.
- SIMULATEDThe fingerprint table in this lesson is produced by
src/de/sim/pipeline.tsin this repository, not measured on a production platform. It is a deterministic model of eight faults against six checks, and it is included because the *pattern* of which checks move together transfers even though the specific dataset does not. - SIMPLIFIEDThe model assumes one fault at a time and a single linear path from source to dashboard. Real platforms fan in and out, and simultaneous faults produce a union of failing checks — the method still works, but the fingerprint no longer matches a single row.
Where the depth lives
This domain teaches how data moves and how you know it arrived intact. It hands the rest off by name.
- — DevOps / Production Engineering owns the change record that this walk consults — deploys, configuration changes, migrations. A large share of transformation faults have a release immediately before them, and correlating the two is often faster than the walk itself.
- — Distributed Systems owns why a count taken at two hops at slightly different moments can legitimately disagree, and what it would take to make the comparison a consistent one.