Timeless

Embedded Time Series Database for Elixir

Hex.pmDocsLicense


"I found it ironic that the first thing you do to time series data is squash the timestamp. That's how the name Timeless was born." --Mark Cotner

TimelessMetrics is an embedded time-series database for Elixir with a Rust-native hot path, built-in HTTP ingest/query APIs, retention, rollups, alerting, scraping, charts, and Prometheus/VictoriaMetrics compatibility.

It runs:

Current Architecture

The default Timeless Stack installation now uses the external timeless-metrics-api Rust owner backed by timeless-libsql; Phoenix remains the control plane and this OTP application is loaded for compatibility and automatic migration without starting a second storage owner. The standalone embedded-library API described below is a separate compatibility surface and now also defaults to the libSQL engine; engine: :rust remains available as the explicit rollback configuration.

For standalone embedded-library users, the libSQL engine is the default. It embeds the timeless-libsql SQLite extension so time-series blocks, the series catalog, rollups, and TimelessMetrics admin data all live in one metrics.db file. The public Elixir and HTTP APIs remain the same. Existing Rust-engine stores must use the verified offline migration described in Operations; libSQL startup intentionally refuses to silently ignore a non-empty rust_engine/ directory.

For the embedded libSQL engine, supported scalar avg/sum/min/max/count queries run in the extension's chunk-aware timeless_aggregate kernel, while latest and latest_multi use its newest-first timeless_latest kernel. Complete, from-aligned buckets for those same five aggregates use its packed timeless_window_batches kernel. Partial terminal buckets, first, last, rate, and other semantic mismatches retain the existing raw fallback. Stored rollup reads use one prepared timeless_rollup_batches call and decode all six aggregates from one versioned blob instead of scanning the tier six times. Wide eager raw reads use one timeless_raw_frame row for the complete result and construct final BEAM series maps in the native decoder; exact reads keep the selective per-series path. Multi-series outer order is unspecified across all engines, while points remain timestamp-ordered within each series.

Hot-path responsibilities live in the Rust NIF:

Elixir still owns the surrounding product surface:

If you need the detailed design, start with docs/architecture.md.

Documentation

Highlights

Quick Start

Add to mix.exs:

{:timeless_metrics, "~> 6.0"}

Add to your supervision tree:

children = [
{TimelessMetrics, name: :metrics, data_dir: "/var/lib/metrics"},
{TimelessMetrics.HTTP, store: :metrics, port: 8428}
]

Write and query:

TimelessMetrics.write(:metrics, "cpu_usage", %{"host" => "web-1"}, 73.2)
{:ok, points} =
TimelessMetrics.query(:metrics, "cpu_usage", %{"host" => "web-1"},
from: System.os_time(:second) - 3600,
to: System.os_time(:second)
)

Memory-only mode:

children = [
{TimelessMetrics, name: :metrics, mode: :memory},
{TimelessMetrics.HTTP, store: :metrics, port: 8428}
]

Opt-in libSQL storage engine:

children = [
{TimelessMetrics,
name: :metrics,
data_dir: "/var/lib/metrics",
engine: :libsql}
]

Elixir API

Writes:

TimelessMetrics.write(:metrics, "cpu_usage", %{"host" => "web-1"}, 73.2)
TimelessMetrics.write_batch(:metrics, [
{"cpu_usage", %{"host" => "web-1"}, 73.2},
{"mem_usage", %{"host" => "web-1"}, 4096.0}
])

Queries:

{:ok, points} =
TimelessMetrics.query(:metrics, "cpu_usage", %{"host" => "web-1"},
from: System.os_time(:second) - 3600
)
{:ok, series} =
TimelessMetrics.query_multi(:metrics, "cpu_usage", %{"host" => "web-1"},
from: System.os_time(:second) - 3600
)
{:ok, buckets} =
TimelessMetrics.query_aggregate(:metrics, "cpu_usage", %{"host" => "web-1"},
from: System.os_time(:second) - 3600,
bucket: {60, :seconds},
aggregate: :avg
)

Discovery and operations:

TimelessMetrics.list_metrics(:metrics)
TimelessMetrics.list_series(:metrics, "cpu_usage")
TimelessMetrics.label_values(:metrics, "cpu_usage", "host")
TimelessMetrics.info(:metrics)
TimelessMetrics.flush(:metrics)
TimelessMetrics.backup(:metrics, "/tmp/metrics-backup")

HTTP API

Core endpoints:

MethodPathDescription
POST/api/v1/importVictoriaMetrics JSON-line ingest
POST/api/v1/import/prometheusPrometheus text ingest
POST/writeInflux line protocol ingest
GET/api/v1/queryLatest-value query
GET/api/v1/query_rangeNative range query
GET/api/v1/exportMulti-series export
GET/prometheus/api/v1/queryPrometheus instant query
GET/prometheus/api/v1/query_rangePrometheus range query
GET/prometheus/api/v1/labelsPrometheus label names
GET/prometheus/api/v1/seriesPrometheus series listing
GET/chartSVG chart
GET/healthLightweight health/status
GET/health/detailedMore expensive store diagnostics

Example ingest:

curl -X POST http://localhost:8428/api/v1/import/prometheus \
-H "Content-Type: text/plain" \
--data-binary '
cpu_usage{host="web-1"} 73.2
cpu_usage{host="web-2"} 61.8
'

Example range query:

curl 'http://localhost:8428/api/v1/query_range?metric=cpu_usage&host=web-1&from=1700000000&to=1700003600&step=60'

Example Prometheus-compatible query:

curl 'http://localhost:8428/prometheus/api/v1/query_range?query=cpu_usage{host="web-1"}&start=1700000000&end=1700003600&step=60'

Benchmarks

The maintained benchmark set lives under bench/:

Notes