block_timestamp values can be delivered after newer ones.
Allium provides delivery metadata tables that give you full transparency into every data update, enabling you to build a reliable and cost-efficient sync pipeline.
Which table answers which question?
The first two are sync tables that drive your ingest pipeline. They are two views of the same delivery metadata, just indexed differently:
snapshots.aggregatedis indexed by delivery time (each row is one batch and the exactblock_timestampranges it carried) — best for pulling new data at the tip of chain.intervals.changesis indexed by data partition (each row is one hour and when it was last touched) — best for full-history partition taint detection (a backfill or patch anywhere in the past).
verified_until, is a quality gate — it answers how far you can trust the data (see Gating on Verification).
Key Concepts
Metadata Tables
delivery_metadata.snapshots.aggregated
Shows the latest state of each snapshot delivered, including what data intervals were loaded in each snapshot. This is the primary table for steady-state syncing.
Each row represents a single snapshot. The
snapshot_loaded_intervals__min_timestamp and snapshot_loaded_intervals__max_timestamp columns tell you the overall time range of data that was loaded. Note that this can be a sparse interval — the snapshot_loaded_intervals__loads and snapshot_loaded_intervals__merged_intervals fields provide the exact sub-ranges if you need finer granularity.
delivery_metadata.intervals.changes
Shows, for each hourly partition of data, when it was last updated. This is the primary table for backfill detection.
For example, if
base.raw.logs has hour = 2025-11-06 02:00 with last_updated_at = 2026-02-20, that means the hourly partition for Nov 6 2AM was last updated (e.g., patched/backfilled) on Feb 20.
Recommended Integration Pattern
For most pipelines we recommend two jobs — not because you need two tables, but because the two access patterns scan very different time ranges: the steady-state job only looks at the tip of chain (recent hours, viasnapshots.aggregated), while the backfill job scans all of history to catch partition taints anywhere in the past (via intervals.changes):
- A steady-state job (high cadence) — handles new data at the tip of chain
- A backfill job (low cadence) — catches late-arriving historical patches
Job 1: Steady-State Sync
Runs every hour (or at whatever cadence your pipeline operates). Picks up newly delivered data snapshots. Querydelivery_metadata.snapshots.aggregated to find snapshots created since your last run:
pull_from_block_timestamp / snapshot_loaded_intervals__max_timestamp to determine the block_timestamp range to pull from the source table.
Job 2: Backfill Sync
Runs daily or weekly. Catches any data that was patched or backfilled for historical time periods — records too old for the steady-state lookback window to catch. Querydelivery_metadata.intervals.changes to find hourly partitions that were recently updated:
hour values are the partitions you need to re-sync. Compare last_updated_at against your own internal tracking timestamp to determine which hours actually need patching.
You can also check last_full_refresh_timestamp to detect if a full table refresh occurred — compare it against your internal timestamp to know if a full re-ingestion is needed.
How the Two Jobs Work Together
Steady-state optimizes for speed and cost — it handles the common case of new data arriving at the leading edge. Backfill ensures full correctness by catching the uncommon case of late-arriving historical patches (data from weeks or months ago being corrected). Together, these two jobs ensure your warehouse stays fully aligned with Allium.Apply changes with delete + insert (not upsert)
Both jobs share the same write rule: for each changedblock_timestamp range, delete all existing rows in that range in your warehouse, then insert the fresh data for that range. Job 1 applies it over the recent lookback window; Job 2 applies it over each patched hourly partition.
The delivery metadata never flags individual rows as deleted — it flags the containing time range as changed. Deletes appear as though the encapsulating time range has changed: a new load range shows up in snapshots.aggregated, and the affected hour gets a fresh last_updated_at in intervals.changes. Follow the changed time ranges and delete + insert each one, and deletions are applied automatically.
Single-table fallback
If you’d rather not maintain two query paths, you can run everything offdelivery_metadata.snapshots.aggregated alone — it carries the exact load ranges of every delivery, including backfills, so both the forward-sync and backfill cases are derivable from it.
The trade-off is on your side:
- A single partition’s update history is spread across multiple snapshot rows (one per delivery that touched it), so a per-partition “is my copy stale?” check means scanning every snapshot whose interval overlaps that partition and taking the latest.
min_timestamp/max_timestampis a coarse envelope — one batch can carry both tip data and a historical patch — so the exact coverage lives in thesnapshot_loaded_intervals__loads/__merged_intervalsJSON. Filtering on the envelope alone re-syncs more than strictly necessary (safe, but costlier).
intervals.changes exists precisely so you don’t have to re-implement that rollup. Use the single-table approach only if your pipeline is simple enough that the extra precision isn’t worth a second query path.
Gating on Verification: delivery_metadata.intervals.verified_until
verified_until reports, per table, the latest point in time up to which Allium has verified the data is complete and made it available in your environment — with no verification gaps before that point.
Gate on it when your use case needs guaranteed, already-verified data (at lower freshness) rather than fast, eventually-consistent data at the tip of chain — i.e. you’d rather wait for verification than consume the leading edge as soon as it lands.
block_timestamp <= verified_until as final, and hold back anything newer until verification catches up.
Reading forward from the start of the window, verified_until advances only while each interval is verified — the first gap (a failed or missing verification) stops it, even if later intervals are verified:
Coverage:
verified_until is currently populated for raw (*.raw.*) datasets. Enriched/derived tables are delivered and tracked by the two sync tables above but may not yet appear in verified_until.dbt Integration Example
If you use dbt with Allium as a source, here’s how you might structure your incremental model:intervals.changes and re-processes the affected partitions.
FAQ: Late-Arriving Data, Deletions, and Finality
Common questions when building a sync pipeline against enriched tables (e.g.dex.trades) where late-arriving data, reorgs, and backfills matter.
Should I sync from per-chain tables or a cross-chain view?
Should I sync from per-chain tables or a cross-chain view?
Anchor on the per-chain tables (
ethereum.dex.trades, arbitrum.dex.trades, …). Cross-chain “all-chains” tables such as crosschain.dex.trades are views — a UNION ALL over the per-chain tables — and views don’t emit delivery events, so they never appear in snapshots.aggregated or intervals.changes. Each per-chain table is delivered independently with its own watermark and its own metadata key. Point your sync jobs at the per-chain keys and the metadata lines up one-to-one.How frequent is late-arriving data, and how much does each batch touch?
How frequent is late-arriving data, and how much does each batch touch?
Late-arriving data falls into two regimes:
- Tip settling (continuous, shallow). A block-hour is delivered within minutes of the chain tip, then re-touched a handful of times over the next few hours as the rolling ingestion window re-scans the leading edge for late-arriving records and shallow reorgs. On EVM chains an hour is typically final within single-digit hours.
- Reprocessing sweeps (infrequent, wide). Periodically a batch job re-derives a large contiguous span of history at once — for example when new DEX-protocol coverage is added and backfilled, or a decoding / USD-pricing methodology is revised and re-applied. These are rare (on the order of a handful per quarter) but can touch a large share of a rolling quarter’s partitions in a single pass, reaching back weeks.
What causes late-arriving data?
What causes late-arriving data?
Two distinct drivers, mapping onto the two regimes above. Tip settling is chain-mechanical — late blocks and shallow reorgs — and is expected and self-healing. Reprocessing sweeps are data-quality / coverage driven — a new protocol or version added to coverage and backfilled, or a revised decoding / pricing methodology re-applied across history. Sweeps are deliberate quality improvements, not incident recovery.
How far back can late-arriving data reach?
How far back can late-arriving data reach?
Well beyond the tip. Reprocessing sweeps routinely revise partitions weeks old on EVM, and months old on the highest-throughput chains. This is the crucial point for pipeline design: a fixed lookback window is necessary but not sufficient. A short steady-state re-pull (24–36h) captures the common case of tip settling but will entirely miss a sweep that reaches back weeks.The correct pattern is the two-job split described in Recommended Integration Pattern: a high-cadence steady-state job with a short lookback, plus a low-cadence backfill job that queries
intervals.changes for any historical hour whose last_updated_at has advanced, with no lookback bound. The backfill job — not the lookback window — is what keeps you aligned against sweeps.How are deletions handled?
How are deletions handled?
Rows are removed — reorgs, and sweep re-derivations that drop previously mis-attributed records. These are hard deletes: there is no
_deleted flag, tombstone, or versioned replacement row. Do not try to infer removals by diffing full snapshots.The metadata never flags an individual row as deleted; it flags the containing hour as changed (a bumped last_updated_at in intervals.changes), so a delete surfaces exactly like an insert or update. Apply changes with a delete-aware re-pull per changed hour — delete every row you hold for that block-hour, then insert the fresh batch — never an upsert/MERGE on the row key. See Apply changes with delete + insert (not upsert) for why a merge strands upstream-deleted rows.How do I know when data is final and safe to process?
How do I know when data is final and safe to process?
Two options, strongest first:
- Settle-gate on
intervals.changes(works today for every table). Treat an hour as ready once itslast_updated_athas been stable for a settle buffer, and keep honoring later advances through the backfill job. For irreversible downstream actions, add margin: an EVM hour is empirically final within single-digit hours in normal operation, but a later reprocessing sweep can still revise it — your backfill job will catch it, so build in a way to reverse or replay affected actions. verified_until(strongest primitive, with a coverage caveat).intervals.verified_untilreports the latestblock_timestampthat is contiguously verified complete and delivered; you gate onblock_timestamp <= verified_until. It is currently populated for raw (*.raw.*) datasets, so enriched/derived tables (e.g.dex.trades) may not yet carry a row. For enriched tables today, the settle-gate plus backfill job is the practical finality mechanism.
is_full_refresh / last_full_refresh_timestamp in the metadata), but you should not expect routine “re-ingest the whole table” events.Can I sync from table columns instead of the delivery metadata?
Can I sync from table columns instead of the delivery metadata?
Some Allium tables expose
_created_at (first write) and _updated_at (bumped on every re-write, including sweeps). Cursoring on _updated_at with a ~1h overlap buffer captures inserts and sweep re-derivations without touching the metadata tables. Three caveats:- These columns are experimental / not officially supported (see Metadata columns).
- A deleted row leaves no
_updated_atto advance past — so you still need the same delete-aware trailing re-pull to apply reorgs and removals. - These tables are partitioned by their timestamp column (e.g.
block_timestamp), so a filter on_updated_atalone does not trigger partition pruning — the query full-scans the target table. Adding ablock_timestampfilter restores pruning, but any late-arriving data in a partition outside that range is then missed. You’re forced to trade scan cost against completeness; the delivery metadata tables avoid the bind because they’re indexed for exactly this lookup.