Timeless

Embedded Time Series Database for Elixir

Hex.pm Docs License


"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:

Method Path Description
POST /api/v1/import VictoriaMetrics JSON-line ingest
POST /api/v1/import/prometheus Prometheus text ingest
POST /write Influx line protocol ingest
GET /api/v1/query Latest-value query
GET /api/v1/query_range Native range query
GET /api/v1/export Multi-series export
GET /prometheus/api/v1/query Prometheus instant query
GET /prometheus/api/v1/query_range Prometheus range query
GET /prometheus/api/v1/labels Prometheus label names
GET /prometheus/api/v1/series Prometheus series listing
GET /chart SVG chart
GET /health Lightweight health/status
GET /health/detailed More 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