Repository navigation
Releases: polythings/timelake
Release list
0.7.0 – 2026-10-18
Release Notes
Added
- Aggregate
time_weighted_mean(time, value [, 'forward_fill' | 'linear']): the mean of irregular readings weighted
by how long each held, over each group's first to last reading; exact, as fast asavg(hourly means of 1,000 tags
over 30 days: 2.0 s). Rollups store its state and answer wider buckets. Settingtime-weighted-mean(auto,
segments,merge) for tables with dedup off whose files overlap. - Aggregate
m4(time, value): a group's first, smallest, largest and last points, as a list of{time, value}.
Rollups store it and answer wider buckets from it (hourly points of 1,000 tags over 30 days: 2.0 → 0.51 s from a
1-minute rollup). After Apache IoTDB's M4. - Rollups of rollups: a table's rollups are refreshed narrowest first, and each is computed from a narrower one where
that is up to date (a daily rollup from the minute rollup: 5.2 → 1.8 s on 30 days of IIoT readings). - Rollups answer coarser queries: a query whose
time_bucketis a multiple of the rollup's width (daily from an
hourly rollup), or that groups by fewer of its columns, is answered by aggregating the rollup's rows again. IIoT,
30 days, a 1-minute rollup: daily statistics per tag 2.1 → 0.25 s, 6-hourly totals 1.1 → 0.11 s. - Setting
newest-first(on by default):ORDER BY time DESC LIMIT kreads time partitions newest first and stops
once older ones can't hold any of the k newest rows (#72). IIoT, 30 days: the 100 newest readings before a past
time 298 → 38 ms; TSBS IoT on local S3: the 10 newest readings 392 → 320 ms. - Setting
in-list-pruning(on by default):column IN (…)filters of more than 20 values skip row groups and pages
whose range holds none of the values, which DataFusion's pruning leaves alone (#103). IIoT, compacted, local disk:
a 100-tag list 54 → 31 ms, a 21-tag list 53 → 26 ms. On object storage it also prunes with a join's runtime
filter: IIoTenergyon local S3 (20 ms per request) 54 → 26 ms repeated, 204 → 183 ms first run. - Setting
filter-pushdown(auto,true,false): the Timelake reader applies filters while decoding Parquet and
decodes the other columns only for rows that pass.auto, the default, turns it on for local files (TSBS geomeans
4–5% faster, selective group-bys up to 40%) and off on object storage, where it adds a round trip per row group. - Data files use the codec in Iceberg's
write.parquet.compression-codecandwrite.parquet.compression-level
(ZSTD 3 by default;lz4,snappy,gzip,brotli,uncompressed). Before, Timelake always wrote ZSTD 3.
Changed
- Breaking: Timelake's scans no longer follow DataFusion's
datafusion.execution.parquet.pushdown_filters; set
timelake.filter_pushdowninstead (see Added). - Breaking (Rust): in
timelake-format,rows::PartitionRowsholds a partition's rows as a list of batches
((Option<i32>, Vec<RecordBatch>)), andContracthas new fields for the new settings. Callers ofsort_and_split
iterate the batches; code that builds aContractfield by field usesContract::from_properties. - Tables created by other engines with a codec other than ZSTD in
write.parquet.compression-codecare now written with
that codec (see Added); tables Timelake created already say ZSTD. - Compaction's merge (also used for appends past 1 GiB) is up to 5× faster on files that alternate series by series,
such as 5-minute batches of every device: 10 M rows of two such inputs merge in 2.6 s instead of 13.9 s. - The block cache sends GETs of at most 1 MiB (storage option
timelake_cache_request_bytes;0for no limit),
longer runs split into parts fetched at once. With 20 ms per request and 50 MiB/s per request, TSBS IoT first
queries 126 → 115 ms (geomean), with 47% more requests.
Fixed
- An append whose string or binary column held more than 2 GiB failed with "offset overflow" (one hour of a 400,000-tag
plant): the rows were concatenated into one batch to sort. Appends now sort groups of up to 1 GiB and merge them. - The block cache fetched the runs of blocks one read needs one after another; it fetches them at once. First
queries on object storage with 20 ms per request: TSBS IoT geomean 162 → 125 ms, one truck's history 233 → 109 ms. - Tigris is recognised when its endpoint comes from
AWS_ENDPOINT_URL_S3(as Fly.io and the Tigris CLI set it), so
readers check for new versions with a LIST there, about 245 ms faster per check than a HEAD (#102).
Install timelake-cli 0.7.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/polythings/timelake/releases/download/v0.7.0/timelake-cli-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/polythings/timelake/releases/download/v0.7.0/timelake-cli-installer.ps1 | iex"Download timelake-cli 0.7.0
| File | Platform | Checksum |
|---|---|---|
| timelake-cli-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| timelake-cli-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| timelake-cli-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| timelake-cli-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| timelake-cli-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
Verifying GitHub Artifact Attestations
The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:
gh attestation verify <file-path of downloaded artifact> --repo polythings/timelakeYou can also download the attestation from GitHub and verify against that directly:
gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation> --repo polythings/timelake0.6.0 – 2026-10-17
Release Notes
Added
- Writes from JavaScript.
TableWriterin@timelake/clientappends to tables from Cloudflare Workers, Node,
Deno, Bun, or browsers, through aStore(r2Storefor an R2 binding,memoryStore, or your own). The
WebAssembly module runs the engine's write path: the same row preparation, Parquet files, commits with retries, last-value
index and head. Rows are objects or an Arrow IPC stream;batchIdmakes retries safe. Appends run inline
maintenance like the engine's (size-tier and cold-partition compaction by streaming merge, withinmaxCompactBytes,
default 64 MiB; retention; snapshot expiry), andwriter.maintain()runs a full pass. Tables are created with
the CLI or Python (SPEC §21.10). In Cloudflare Workers, import from@timelake/client/workersand pass the
module from@timelake/client/timelake.wasmtoinitWasm. Measured in workerd: an append of 100,000 rows takes
0.7 s and 37 MiB, compaction of a day of 24 files 2.7 s and 95 MiB (bench/results/WORKERS.md). data-page-rows: rows per Parquet data page in the files a table gets (default 20,000; 8,192 for files
written from JavaScript, where compaction holds a page per column of each file it merges).- Version check interval. The setting
version-check-interval(table or session) says how long the Timelake
reader uses a table version it loaded or checked before checking for a newer one. Within it, queries send no
request for the table's version, and can miss commits by other processes; the process's own commits are seen at
once, and writers always check. The default, 1 second, is the old fixed behaviour;0schecks on every query
(SET timelake.version_check_interval = '30s',--set,Lake(settings=…), ortimelake alter --set). - Ingestion guides. The Quick start has eight sub-pages that set up
continuous ingestion, each with several designs, a recommendation, scheduled checks on recent data, and
maintenance: Local, AWS (S3, Kinesis, SQS, IoT Core, and Firehose into Lambda), Azure (Blob Storage, Event Hubs,
Service Bus, and IoT Hub into Azure Functions), Google Cloud (Cloud Storage and Pub/Sub into Cloud Run functions),
Cloudflare (R2 and Queues, appended by a consumer Worker withTableWriteror by a writer script), Fly.io, Vercel,
and Tigris. Each platform has an
overview, a setup page, a page per design, and a checks page; the docs sidebar opens and closes nested pages. The
Quick start compares the platforms and designs. The AWS page compares S3 with S3 Tables.
Changed
- Compaction streams. Compaction merges its input files as sorted streams instead of loading a partition into
memory, so its memory no longer grows with the partition. Full compaction of TSBS IoT (72 hourly files into 3 days):
3.65 → 2.37 s, peak memory 1.95 → 0.53 GB, with identical files. - Columns are encoded in parallel. Timelake writes Parquet with its own writer (shared with the WebAssembly
writer), encoding each row group's columns on threads of their own. The files are the same, byte for byte. The
compaction above takes 1.17 s instead of 2.37 s; importing 11.7M rows 4.7 s instead of 6.0 s.
Fixed
- Docs: the storage page's check for conditional writes created a table twice, which fails the second time on any
store. It now sends a put-if-absent twice with the AWS CLI. The quick start no longer says that Timelake reads AWS
profiles: it reads theAWS_*environment variables (aws configure export-credentialsexports a profile's). The
storage page says to create a Tigris lake bucket as single-region or multi-region: on Tigris's defaultglobal
buckets, conditional writes are checked within one region only. @timelake/clientdidn't run in Cloudflare Workers: Workers can't compile WebAssembly from bytes at runtime,
which the client did on first use. The@timelake/client/workersentry takes the compiled module instead.
Install timelake-cli 0.6.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/polythings/timelake/releases/download/v0.6.0/timelake-cli-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/polythings/timelake/releases/download/v0.6.0/timelake-cli-installer.ps1 | iex"Download timelake-cli 0.6.0
| File | Platform | Checksum |
|---|---|---|
| timelake-cli-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| timelake-cli-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| timelake-cli-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| timelake-cli-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| timelake-cli-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
Verifying GitHub Artifact Attestations
The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:
gh attestation verify <file-path of downloaded artifact> --repo polythings/timelakeYou can also download the attestation from GitHub and verify against that directly:
gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation> --repo polythings/timelake0.5.0 – 2026-10-17
Release Notes
Added
- Dedup on read. In tables with dedup on, the Timelake reader (CLI, Python, Rust) returns only the latest write of
each(series, time), before compaction has removed the older copies. Only files that may share a key are merged;
the rest are read as before. DuckDB, other Iceberg readers and the TypeScript client still see the copies until
compaction. It can be turned off per table (timelake.read-dedup = false) or per session
(SET timelake.read_dedup = false, or--set timelake.read_dedup=falseon the CLI). A second strategy,
timelake.read-dedup-strategy = keys-first, lists the older copies first and leaves them out of a normal scan. - Cold partitions compact sooner. Once newer data is
timelake.compact-cold-after(default 1 hour) past a
partition's end, inline maintenance rewrites the partition into sorted, duplicate-free files when it has two small
files, even if the append didn't touch it. - Insertion time. Time-series tables get an
_ingest_tscolumn: each append fills it with the time the append
started, unless the input brings its own values (an import keeping arrival times). Among rows with the same
(series, time)the latest insertion wins, then the later commit, everywhere: within an append, on read, in
compaction and in the last-value index. DuckDB can pick the same rows with
QUALIFY row_number() OVER (PARTITION BY <series>, <time> ORDER BY _ingest_ts DESC) = 1. Leave it out with
ingest_time=False(Python) or--no-ingest-time(CLI);timelake infoshows it. - Settings. Every switch is declared in one registry, with its default, the levels it can be set at (creation,
table, session, call) and the version that added it.timelake settingslists them, and with a table name shows
the table's values;timelake alter <table> --set read-dedup=false --unset retentionchanges a table's settings
(Table.set_propertiesin Python and Rust).Lake(..., settings={...})in Python sets session settings for every
query, andSHOW timelake.read_dedupshows a session's value. Names work with dashes or underscores, with or
withouttimelake.. The docs page "Table properties" is now "Settings". - Rollups.
CREATE MATERIALIZED ROLLUP iot.readings_1h AS SELECT device, time_bucket(INTERVAL '1 hour', time) AS bucket, avg(value), max(value), count(*) FROM iot.readings GROUP BY device, bucketcreates a table of aggregates
per time bucket and computes it. Maintenance keeps it up to date: inline after appends once its last refresh is
older thantimelake.rollup-refresh-interval(default 1 minute), and on everytimelake maintain;
REFRESH MATERIALIZED ROLLUPrefreshes it now andDROP MATERIALIZED ROLLUPremoves it. AGROUP BY time_bucket(…)
query on the raw table with the same width and grouping reads the buckets the rollup has right from it, and the rest
(buckets changed since the refresh, partial buckets at the edges of the time range) from the raw table, so the
answer is the same as without it;SET timelake.rollup_routing = falseturns that off. First version: one level from
the raw table, grouped by series-key columns, withcount,sum,min,max,avg,first_valueand
last_value.timelake maintainandTable.maintain()reportrollups_refreshedandrollup_rows
(--no-rollups,rollups=Falseto skip). The docs have a new page, "Rollups". - Block cache. Reads of files a table never changes (data files, manifests, manifest lists, last-value index
files) go through a cache of 1 MiB blocks, in memory by default on object stores and HTTPS (256 MiB), and on local
disk withtimelake_cache=disk, shared by every process on the machine (4 GiB). A query still checks for a newer
table version, so answers are as fresh as before. Storage optionstimelake_cache,timelake_cache_dir,
timelake_cache_memory_bytes,timelake_cache_disk_bytesandtimelake_cache_block_bytes;Lake.cache_stats()
in Python andStorage::cache_stats()in Rust count hits and misses. On S3, TSBS IoT repeat queries went from
54 to 16 ms with no requests, and 16 clients from 288 to 398 queries per second.
Changed
- Table format 0.3.0 for new tables (the insertion-time column). Timelake 0.4 and older can't open them: upgrade
every reader and writer first.timelake upgraderaises older tables to 0.3.0 but doesn't add_ingest_ts(a
table's columns are fixed at creation for now). - Settings are checked. Creating or altering a table with an unknown
timelake.*property, a bad value, or a
setting fixed at creation (time-column,series-key,chunk-interval,dedup,ingest-time-column) as a
property is an error; use the table options for those.timelake.format-versioncan no longer be set by hand.
SETof a table-only setting in SQL is an error.timelake sql --setwith a bad key reports an error instead of
crashing. - Rust:
SessionSettingshasgetandflaginstead of public fields.Session::parsereturns
timelake::Statement(DataFusion's statement, or a rollup statement) forSession::execute.
MaintainOptionshas arollupsfield andMaintainReporthasrollups_refreshedandrollup_rows. - With dedup on, rows with the same
(series, time)within one append keep only the last, so the append'srows
counts them once. Files written by earlier versions may still hold such copies until compaction. - Benchmarks: Timelake's repeat runs on object storage now read from its memory cache, so their times drop from
this version on; first runs, in a fresh process, are unaffected.timebench run --reusealso runs the concurrent
dashboards, andTIMEBENCH_TIMELAKE_OPTIONSpasses storage options to Timelake's readers. - The TypeScript client's WebAssembly reader is about 4% smaller (6.7 MB), built with Rust 1.99.
Fixed
- Compaction could let an older write win. Inline compaction merged files of a similar size even when a file of
another size was committed between them, and the merged file then counted as newer than that file: where both held
the same(series, time), the older value won from then on. Compaction now merges only files next to each other in
commit order, and a merged file keeps the data sequence number of the newest file it replaces.
Install timelake-cli 0.5.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/polythings/timelake/releases/download/v0.5.0/timelake-cli-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/polythings/timelake/releases/download/v0.5.0/timelake-cli-installer.ps1 | iex"Download timelake-cli 0.5.0
| File | Platform | Checksum |
|---|---|---|
| timelake-cli-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| timelake-cli-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| timelake-cli-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| timelake-cli-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| timelake-cli-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
Verifying GitHub Artifact Attestations
The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:
gh attestation verify <file-path of downloaded artifact> --repo polythings/timelakeYou can also download the attestation from GitHub and verify against that directly:
gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation> --repo polythings/timelake0.4.0 – 2026-10-14
Release Notes
Added
- Install from the usual registries:
pip install timelake,cargo install timelake-cli(or thetimelake
library crate), andnpm install @timelake/client. The CLI also has one-line installers for macOS, Linux and
Windows and binaries on each GitHub release, with build
attestations. See the Quick start.
Changed
- Table format versions are semantic versions, separate from Timelake's: tables created now record
timelake.format-version = 0.2.0. Tables marked2by Timelake 0.3.0 read as 0.2.0 and need no upgrade; tables
without the property are 0.1.0. A newer patch format is read and written by older Timelakes, a newer minor format
(before 1.0) by neither, with a message to upgrade Timelake. See
Upgrading. Timelake 0.3.0 can't open tables created from now on:
upgrade every reader and writer first.
Install timelake-cli 0.4.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/polythings/timelake/releases/download/v0.4.0/timelake-cli-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/polythings/timelake/releases/download/v0.4.0/timelake-cli-installer.ps1 | iex"Download timelake-cli 0.4.0
| File | Platform | Checksum |
|---|---|---|
| timelake-cli-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| timelake-cli-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| timelake-cli-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| timelake-cli-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| timelake-cli-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
Verifying GitHub Artifact Attestations
The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:
gh attestation verify <file-path of downloaded artifact> --repo polythings/timelakeYou can also download the attestation from GitHub and verify against that directly:
gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation> --repo polythings/timelakeTimelake 0.3.0
Added
- Table format version: tables record
timelake.format-version(2 for tables created now; tables without it are
format 1). Timelake reads tables of any older format and refuses to write to a table of a newer format than it
knows, with a message to upgrade. timelake upgrade(alsoTable.upgrade()in Python and Rust) brings tables to the current format: records the
format version, builds a missing last-value index, rewrites unsorted or small files and writes the head. Each step is
a commit, and no snapshot expires for 7 days (--keep-snapshots), sotimelake rollback <table> <snapshot>
(Table.rollback()) undoes it.--dry-runshows the plan. New table propertytimelake.keep-snapshots-until.
Fixed
- After a rollback, a batch id from the abandoned snapshots no longer counts as already committed.
Changed
- Faster first queries: after each commit the writer also writes
metadata/timelake-head, which holds the table
metadata, the live files, and (while they fit in 1 MiB) the last-value index and the Parquet footers of the newest
files. A cold query reads it together with a listing of the table's metadata files that confirms it is current, so a
firstlatestquery takes one round trip to storage instead of three, and a first filtered scan two instead of six.
A process that already has a table only checks for a newer version. Measured from a laptop, first queries take
245–545 ms instead of 510–1,030 on S3, and 210–520 instead of 760–1,190 on Tigris. The TypeScript client reads the
head too and checks for a newer version while its query runs (TableInfo.manifestsisnullfor a table read from
its head). Other engines see no change. New table propertiestimelake.head-format(arrow,json, a codec
registered withLake::register_head_codec, ornone) andtimelake.head-inline-bytes; new storage options
timelake_version_lookup(listorhint: no LIST, one more round trip),timelake_freshness_check(headorlist,
listby default on Tigris) andtimelake_warm_connections(off by default). The requests each operation makes,
and what they cost, are in the storage docs. If you write a table with an older Timelake after this one, delete its
metadata/timelake-head.
Timelake 0.2.0
Tagged after the fact: the code at this tag still reports version 0.0.1.
Changed
- Faster commits: an append uploads its data files and metadata all at once, then makes the one conditional
write, so it waits for two round trips to storage instead of five. The version hint is now updated in the
background; Rust programs should calltimelake::flush()before exiting (the CLI and the Python package do).
Full changelog: https://github.com/polythings/timelake/blob/main/CHANGELOG.md
Timelake 0.1.0
The first version. Tagged after the fact: the code at this tag reports version 0.0.1.
Added
- Engine (
timelakecrate): time-series and plain tables stored as Parquet files with Apache Iceberg metadata, on
S3, GCS, Azure, S3-compatible stores (R2, MinIO, RustFS) or a local disk. Read-only access over HTTPS.- Time-series tables are partitioned by
hour,dayormonthand sorted by(series key, time). - Every append is one atomic commit. Concurrent writers are safe without a catalog service (put-if-absent on
vN.metadata.json); a conflicting commit is rebased and retried. - Idempotent appends with a
batch_id: a repeated batch is skipped and reported as 0 rows. - A last-value index per snapshot gives the newest row of every series without a scan.
- Maintenance (compaction, deduplication where the latest write wins, retention, snapshot expiry) runs inline after
appends, or on demand. A maintenance failure is reported separately and never undoes the append. - SQL through DataFusion, with
time_bucket(), file, row group and page pruning, and "latest row" queries rewritten
to read the index.
- Time-series tables are partitioned by
- CLI (
timelake):create,append,import,sql,latest,maintain,info,ls,duckdb,dropand
index. Loads Parquet, CSV, gzipped CSV and TSV. - Python (
timelakepackage):LakeandTablewithcreate_table,append(pyarrow, polars or any Arrow
stream),sql,latestandmaintain, returning pyarrow tables. Full type stubs. Invalid input raises
ValueError, a missing tableKeyError, and an unresolvable concurrent committimelake.ConflictError. - WebAssembly reader and TypeScript client (
@timelake/client): reads tables over HTTPS in the browser or Node,
with latest, range and time-bucket queries and Zod-typed rows. - Open format: plain DuckDB, Spark and PyIceberg read every table.
timelake duckdb <table>prints the SQL.
Full changelog: https://github.com/polythings/timelake/blob/main/CHANGELOG.md