Timelaketimelake
DocsBenchmarksRoadmapSpecContribute
Apache 2.0 ·
Roadmap

DoneIn progressPlanned

Tables and storage

Done:1. Iceberg tablesDone:2. CommitsDone:3. Object-store catalogIn progress:4. Time-series table contractIn progress:5. File layout and encodingsIn progress:6. Last-value indexDone:7. One-request table state and version checksDone:8. Table format versions and upgradesDone:9. SettingsPlanned:10. Supplemental timestamps

Writing

Done:11. AppendsDone:12. Late data and dedup on readDone:13. Writes from WebAssemblyPlanned:14. Hot tier and seconds-fresh readsPlanned:15. BackfillIn progress:16. Import and migrationPlanned:17. DeletesPlanned:18. GDPR erasure

Maintenance

Done:19. CompactionIn progress:20. Maintenance runs, retention and expiry

Reading

In progress:21. Timelake readerDone:22. Block cacheDone:23. Filter pushdownDone:24. Pruning with long IN listsDone:25. Newest-first scansDone:26. Scans in series orderDone:27. Materialized rollups

SQL functions

In progress:28. Function libraryDone:29. Time buckets and gap fillingDone:30. Signal aggregatesDone:31. Downsampling with m4

Integrations

Planned:32. REST catalogPlanned:33. DuckDB extensionPlanned:34. Snowflake connectionPlanned:35. Multitenancy

Services

Planned:36. Ingestion and rulesPlanned:37. Data to eventsPlanned:38. Static read modelsPlanned:39. Computed metricsPlanned:40. Agent access with permission setsPlanned:41. Device syncPlanned:42. Edge gatewayPlanned:43. Server mode

Benchmarks

In progress:44. Benchmark harness: timebenchIn progress:45. Industrial IoT benchmarkPlanned:46. Agent benchmark

Background

ArchitectureDeployment and cost modelEmbedded engine: DataFusionLandscape and positioningLicense and open corePurpose and requirementsReleases
TimelaketimelakeThe open time-series lakehouse.
v0 · Apache 2.0 · Contribute · GitHubBuilt on Apache Arrow, Apache DataFusion, Apache Parquet, and Apache Iceberg.
DocsBenchmarksRoadmapSpecContribute

Spec 15 · Writing

Backfill

PlannedUpdated 2026-10-11View on GitHub
Versions
Not released yet
Roadmap
Data lifecycle, Technical preview
Related
14. Hot tier and seconds-fresh reads, 16. Import and migration, 19. Compaction

Summary

Load months or years of history into a live table with one command: written in the final layout, staged on an Iceberg branch, validated, published atomically, resumable, with rollups refreshed afterwards. Existing rows win on conflict by default.

Design

Backfill means loading historical data (months to years) into a table, usually while live ingestion continues. Common IoT cases:

  • Onboarding a site that already has a historian (PI, Ignition, …)
  • Migrating from TimescaleDB, InfluxDB, or S3 Tables + Glue
  • An edge gateway flushing days of buffered data after an outage
  • Reprocessing a range after a calibration fix

Goals:

  • One command.
  • No small files.
  • No disruption to live ingestion or queries.
  • All-or-nothing visibility.
  • Resumable.
  • Rollups correct afterwards.
  • No false alarms.

How it works

  1. Plan: Read the source and map its columns onto the table contract (time column, series key, types, time zone). Split the work into units of time chunk × series bucket.
    • --dry-run reports row count, time range, overlap with existing data, rows that fall outside retention, and estimated cost.
  2. Write in the final layout: Workers write files that are already sorted, sized, and partitioned at the granularity appropriate for the data's age.
    • This bypasses the WAL, the hot tier, and later compaction, so nothing gets written twice.
    • Work units are independent, so the job fans out across threads, processes, or Lambdas.
  3. Stage on an Iceberg branch (backfill/<job_id>), the write-audit-publish pattern. Readers of main see nothing yet.
  4. Validate: Row counts vs. the source, dedup-key conflicts, time bounds, null checks, and optional user SQL assertions.
  5. Publish: Apply the branch's changes onto the current main in one atomic commit.
    • Live ingestion keeps committing meanwhile; append-only commits don't conflict.
    • Very large jobs can publish chunk by chunk (e.g. one month at a time) instead.
  6. Follow-up: The published time ranges are marked as changed, so the rollups over those ranges are refreshed in bulk, in parallel by chunk.

Overlap modes (what happens when backfilled rows meet existing rows)

ModeBehaviorTypical use
merge (default)Dedup by dedup_key. Conflicts are resolved at write time. By default existing rows win (prefer = existing); override per job with prefer = backfill.Migrations, gateway catch-up
replaceAtomically replaces the covered (series, time range) in one commit. Replaced rows are logically deleted (spec 17), so it is reversible.Reprocessing after calibration fixes
fill_gapsInserts only into (series, time range) spans that have no existing dataPatching outages from a secondary source
appendNo overlap checks (fastest)Loading into an empty table

Interactions with the rest of the system

  • Rules and alarms: Not evaluated by default.
    • --evaluate-rules runs them in historical mode. Resulting events are written with source = 'backfill' and are never sent to real-time sinks.
  • Retention: Rows older than the table's retention are rejected in the dry run unless the job overrides retention.
  • Compaction: Backfilled files are already compacted. The compactor skips partitions with an unpublished backfill on the same range.
  • Late bulk data from live sources:
    • v1: Manual and opt-in. Users run a backfill for a gateway's buffered data, or mark an ingest stream / batch as backfill.
    • Later: timelake-ingest automatically routes large batches older than a threshold to the backfill writer, avoiding WAL floods and small files in old partitions.
  • Resumable and idempotent: Job state (units done or pending) is stored in the catalog or object storage. Each work unit has an ID, so timelake backfill resume <job> is safe to re-run.

Sources: Any source supported by import (spec 16).

Interfaces:

timelake backfill iot.sensor \
  --from 's3://old-historian/export/*.parquet' \
  --map ts=timestamp,device_id=tag_name --tz Europe/Berlin \
  --mode merge --dry-run
timelake backfill status <job>      # progress, throughput, ETA
timelake backfill resume <job>
job = lake.table("iot.sensor").backfill(source=df, mode="replace",
                                         start="2024-01-01", end="2025-01-01")
job.wait(); print(job.report())
-- DuckDB extension
CALL timelake_backfill('iot.sensor',
  'SELECT * FROM read_csv(''s3://exports/*.csv'')', mode := 'merge');

Decisions

IDDecisionStatus
D9Backfill is a first-class operation: one command, final layout on write, staged on an Iceberg branch, atomic publish; existing rows win on conflict by default (spec 15)Accepted

Open questions and issues

None open.

History

  • 2026-10-11: D9 accepted (SPEC.md §8.4); Q14 and Q15 resolved
  • 2026-10-19: moved from SPEC.md into its own spec.
  • 2026-10-19: was spec 10; specs regrouped one per feature, with milestones on the roadmap.