skip to content

Why does a dbt snapshot miss changes that occur between two snapshot runs?

level: middleimportance: should knowfreq 45%

answer

  1. it looks, it does not watch
  2. between two runs, nobody is home
  3. only the final value survives the interval
  4. your schedule sets the resolution of history
  5. re-running tomorrow cannot recover yesterday

basics

~20 s

A dbt snapshot samples the source's current state each time it runs. If a row moves through several values between runs, only the last one is seen, so intermediate versions are never recorded and cannot be recovered later.

solid answer

~40 s

A snapshot is not a change feed; it is a periodic comparison against whatever the source holds at that instant. If an order goes `pending` → `packed` → `shipped` between two nightly runs, dbt sees only `shipped` and writes one version, so the history shows `pending` jumping straight to `shipped`. This holds under both strategies: the `timestamp` strategy reads only the row's current `updated_at`, not every value it passed through. Your snapshot schedule therefore sets the resolution of your history, and the gap is permanent — running the snapshot again tomorrow cannot recover yesterday's intermediate values. The fixes are to run the snapshot more frequently, accepting more rows and more warehouse cost, or to stop sampling and consume an upstream change stream or application event log instead.

code

text · 9 lines
text
source changes between two nightly runs
  09:00  status -> packed
  14:00  status -> shipped
  16:00  status -> delivered

what the 02:00 snapshot records
  id  status     dbt_valid_from  dbt_valid_to
  42  pending    2024-03-01      2024-03-02
  42  delivered  2024-03-02      NULL

go deeper

for a junior

Remember that a snapshot only sees the source at the moment it runs, so anything that changed and changed again in between is recorded as a single jump.

for a middle

Explain why both strategies share this blind spot — the timestamp strategy reads one current timestamp, not a history — and that snapshot cadence is what sets the resolution of the stored windows.

for a senior

Demonstrate operational judgment: monitor snapshot recency, be explicit with stakeholders about what nightly windows can and cannot support, and recognise when the requirement genuinely needs an upstream change feed.

for a principal

Own the tradeoff between resolution, storage growth and run cost across the whole project, and decide when history should be sourced from an event log the business owns rather than inferred by periodic sampling.

## Snapshots sample; they do not observe Every `dbt snapshot` run does the same thing: read the source as it exists right now, compare it to the open rows in the history table, close and insert where something differs. Between two runs, dbt is not watching. Whatever the source did in that interval leaves no trace beyond its final value. Suppose an order's `status` moves at 09:00 to `packed`, at 14:00 to `shipped`, and at 16:00 to `delivered`, and the snapshot runs nightly at 02:00. The next run sees `delivered` and writes exactly one new version. The history now claims the order went from `pending` straight to `delivered`, with `packed` and `shipped` never having existed. No later run repairs this: the source no longer holds those values, and the snapshot table only ever appends what it was shown. ## Both strategies have the same blind spot It is tempting to think the `timestamp` strategy avoids this because it reads a real modification time. It does not. dbt reads the row's *current* `updated_at` — a single scalar describing the most recent write. Three intermediate updates leave one timestamp behind. The `timestamp` strategy dates the recorded version accurately; it does not reveal the versions it never saw. The `check` strategy is identical in this respect and additionally dates the version with the run time. ## Snapshot cadence is a resolution decision Put plainly: **your schedule is your history's resolution.** A daily snapshot answers "what did this row look like on a given day" and nothing finer. An hourly snapshot answers hourly questions and multiplies row counts accordingly. This should be a deliberate choice driven by what the business asks: - Reporting end-of-day dimension state for a customer mart? Daily is fine and cheap. - Auditing how long orders sat in each fulfilment stage? Sampling is the wrong tool entirely — you need the transitions themselves. - Compliance questions about who held which entitlement at which moment? Sampling will not satisfy them, and saying so early is better than discovering it during an audit. Running snapshots more often is a legitimate lever, and a snapshot is usually cheap enough to schedule separately from your main `dbt build` — many teams run `dbt snapshot` on a tighter cadence than the rest of the project precisely for this reason. But there is a floor: no schedule catches changes that happen and reverse between runs, and a row that flips A → B → A is invisible at any interval that misses the excursion. ## When sampling is the wrong tool If missed transitions actually matter, the fix is upstream, not in dbt configuration. Either the source system emits an event or audit log recording each transition, or the ingestion layer captures changes from the database's transaction log; in both cases dbt's job shifts from snapshotting mutable state to modelling an already-complete history. A snapshot on top of a change feed is redundant. Be explicit about which world you are in, because "we snapshot nightly" quietly becomes "we have full history" in a stakeholder's head unless someone says otherwise. ## Practical guardrails - **Document the cadence next to the snapshot** in `schema.yml`, so consumers of `dbt_valid_from` know the windows are quantized to the schedule. - **Do not let the schedule drift silently.** A snapshot that stops running does not fail loudly — models still `ref()` it and still return rows, they are just increasingly stale, with every entity frozen in whatever state it held on the last successful run. A recency check on `max(dbt_updated_at)` catches this. - **Beware the catch-up illusion.** After an outage, the first successful run does not backfill the missed days; it writes one version dated now. If someone asks you to "backfill the snapshot", the honest answer is that source state for those days no longer exists. - **Watch for reverting values.** Fields that oscillate — a score, a risk band, an availability flag — are the ones sampling distorts most, and they are the best candidates for pushing upstream to an event-based source.

  • Your dbt snapshot did not run for three days. What does the first successful run afterwards record?
    One new version per changed entity, reflecting the source's state right now. The three missing days are gone: dbt has no way to reconstruct what a mutable row held on days it never read. Under the timestamp strategy the new version at least carries the true source updated_at; under check it is dated with the run time. Treat it as a permanent gap and tell consumers.
  • Does running the snapshot hourly instead of daily guarantee you capture every change?
    No. It raises resolution, not completeness. Any change that happens and is superseded within one interval is still invisible, and a value that flips away and back between runs looks like nothing happened. Hourly also multiplies row counts and run cost. If every transition must be recorded, the answer is an upstream event log or change feed, not a tighter schedule.

saying these in an interview costs you the question

  • Says a later snapshot run backfills the missed days
  • Thinks the timestamp strategy captures every intermediate value
  • Promises full audit history from a nightly snapshot
  • Assumes a stalled snapshot fails loudly downstream
  • Believes more frequent runs guarantee completeness

context