On this page

The pipeline that reported success while doing nothing

An identification outage that lasted thirteen days, three failed fixes, and what we changed about how we watch things.


The symptom

A customer's dashboard said identification had stopped yesterday. The truth was thirteen days, and every system involved was reporting healthy.

tracking_events         8,191 in 24h, most recent seconds ago   ← pixel fine
liveintent_resolutions  ~1,200/day, most recent minutes ago     ← vendor fine
signal_account_visitors last created 2026-08-29 13:43           ← nobody new
unified_profiles        last created 2026-08-29 13:43           ← nobody new

Traffic arriving, resolutions arriving, and not one new identified person in thirteen days.

Three things that were all green

The schedule. The hourly job was not paused, was firing on time, had nothing stuck, and showed ten recent actions. Looking at the scheduler told you everything was fine.

The updated_at column. Rows were being written every hour, so any monitor watching row freshness saw a live system. Those writes were a different job — an hourly intent recompute touching existing rows. updated_at moved; created_at had not moved in thirteen days.

The workflow status. In an earlier round of the same outage, the workflow caught its own exception and returned a result object with status='failed'. Temporal saw a COMPLETED workflow. A caught failure returned as a value is a completion.

The actual cause

Every hourly run failed in two seconds. The error surfaced as a circuit-breaker trip, which was itself a mask — the first attempt raised the real error, the second returned CircuitOpenError, and only the second was reported:

CheckViolationError: new row for relation "liveintent_ingestion_runs"
violates check constraint "liveintent_ingestion_runs_status_check"
DETAIL: Failing row contains (…, no_data, 0, 0,
        discovery found no dated partitions under the configured prefix).

The job had a branch for "the vendor delivered nothing", which recorded status='no_data'. The CHECK constraint permitted running | success | partial | failed. Every one of those writes was rejected, the retries exhausted, the breaker opened, and the workflow died on its bookkeeping call — before the step that turns resolutions into people.

SELECT DISTINCT status returned {success, failed}. The value no_data had never once landed.

Each fix planted the next one

What broke Duration
1 Discovery hit an S3 AccessDenied; the workflow caught it and returned a value, so the scheduler saw COMPLETED 3d 7h
2 The fix for #1 recorded the failure with an empty date; the recorder died on date.fromisoformat('') and tripped the breaker 4 days
3 The fix for #2 added the no_data branch, and nobody widened the constraint 13 days

All three were in the same recorder. The common factor was not carelessness: the status vocabulary had two homes — string literals in the job, a CHECK constraint in SQL — and no test read both.

The design error underneath

Recording what happened must not be able to prevent what happens.

The write that logged the run was awaited on the critical path, ahead of the step that produced the product's entire value. A logging concern was a hard dependency of the business. It took identification down twice before anyone named it.

What we changed

  • One vocabulary, one home. The statuses are declared once in code, and a test asserts that set equals the CHECK constraint as written in the migration. Adding a status without a migration now fails a test instead of production.
  • The recorder cannot fail the run. It logs loudly and returns. Safety comes from the absence of the row: a monitor watches run freshness with a three-hour budget and goes stale precisely when this breaks.
  • The banner measures the output, not the job. It had read "when did our ingest last run", which is a different fact from "when did we last identify someone" — and a crashed run refreshed it, which is how thirteen days displayed as one. It now reads the last actual identification, on a threshold derived from the measured gap distribution (p50 1.0h, p90 2.0h) rather than chosen.

What to take from it

Ask what would make the number move. A monitor that cannot go red is decoration. Before trusting one, work out which specific failure it would catch — and check that a failure cannot refresh the thing it measures. Our banner, our updated_at, and our workflow status all had that defect in different forms.

Distinguish "the job ran" from "the job did something." They are different questions with different right answers, and fusing them lets a healthy job speak for a dead pipeline.

A test that reads one side of a contract is not a test of the contract. The constraint and the code each said something true about themselves. Nothing read both.

This page is maintained next to the service it describes and rendered here. Read it as markdown if you are pointing an agent at it.