metrics

Time-series

Retention, downsampling, native window queries.

What it is

Time-series on RedDB.

Native time-series collection with retention policies, materialised views for downsampling, and time_bucket() window functions baked into the SQL grammar.

Same engine as your relational tables, so you can join an events time-series to a users table in one query — what InfluxDB → Postgres sync jobs exist to avoid.

Powers the metrics surface RedDB itself ships (KPI cards, distribution histograms) so the abstraction is battle-tested by the platform.

Code

Write it. Read it. Same engine.

Insert

INSERT INTO cpu_metrics (metric, value, tags) VALUES ('cpu.idle', 95.2, {"host":"srv1"});

Query

SELECT time_bucket('5m', timestamp) AS bucket, AVG(value)
FROM cpu_metrics
WHERE metric = 'cpu.idle'
GROUP BY bucket ORDER BY bucket DESC LIMIT 12;

Use cases

Where time-series earn their place.

  • Application metrics

    Latency p99, error rate, queue depth — everything you would otherwise pipe to Influx.

  • Product analytics

    Event streams aggregated by minute / hour / day, with retention policies that prune the raw rows automatically.

  • Billing usage

    Per-customer resource consumption with materialised monthly rollups for invoicing.

  • IoT telemetry

    High-cardinality device tags filtered and downsampled at write time.

Build it

End-to-end walkthrough.

Four steps from empty database to a real time-series workload — each step shows the exact code and explains what the engine is doing.

  1. Step 1

    1. Declare the time-series

    Retention is engine-managed — the storage drops chunks older than 90 days automatically, no cron job. CHUNK INTERVAL is the partition granularity; 1 day is sane for typical metric workloads.

    CREATE TIMESERIES metrics
      RETENTION 90 d
      CHUNK INTERVAL 1 d;
  2. Step 2

    2. Insert points

    Tags are JSONB so you can store arbitrary dimensions without schema changes. The engine indexes the high-cardinality metric + tags axis automatically — queries filtering by host, region, etc, are sub-linear in row count.

    INSERT INTO metrics (timestamp, metric, value, tags) VALUES
      (now(), 'cpu.idle',     95.2, '{"host":"srv1","region":"gru"}'),
      (now(), 'cpu.user',      3.1, '{"host":"srv1","region":"gru"}'),
      (now(), 'memory.used', 4096,  '{"host":"srv1","region":"gru"}');
  3. Step 3

    3. Aggregate with time_bucket

    time_bucket is a window function over the timestamp axis — the dashboarding workhorse. Combine with standard aggregates (AVG, percentile, MAX) and grouping by tags to get the chart-ready shape in one query.

    SELECT
      time_bucket('5 minutes', timestamp) AS bucket,
      tags->>'host'                      AS host,
      AVG(value)                         AS avg_idle,
      PERCENTILE_CONT(0.99)
        WITHIN GROUP (ORDER BY value)    AS p99_idle
    FROM metrics
    WHERE metric = 'cpu.idle'
      AND timestamp > now() - INTERVAL '6 hours'
    GROUP BY bucket, host
    ORDER BY bucket DESC;
  4. Step 4

    4. Materialise a continuous aggregate

    Materialised views collapse millions of points into pre-aggregated rows the engine refreshes incrementally. The dashboard hits the view, not the raw collection — KPI queries resolve in milliseconds even after months of accumulated data.

    CREATE MATERIALIZED VIEW mv_metrics_kpi_1d AS
      SELECT
        time_bucket('1 day', timestamp) AS bucket,
        metric,
        AVG(value) AS avg_value
      FROM metrics
      WHERE metric IN ('cpu.idle','memory.used','storage.used','connections.active')
      GROUP BY bucket, metric;
    
    -- Daily KPI dashboard reads from the view, not from raw points.
    SELECT * FROM mv_metrics_kpi_1d
    WHERE bucket > now() - INTERVAL '30 days'
    ORDER BY bucket;

Engine features

What RedDB adds on top of time-series.

The engine knobs you would otherwise wire up yourself — TTL, encryption, indexes, atomic ops, eventual consistency — applied to this data model.

  • Retention (chunk-level)

    CREATE TIMESERIES … RETENTION <duration> drops whole chunks once they age out — no per-row delete, no vacuum cycle. Storage stays bounded automatically.

  • time_bucket window functions

    time_bucket('5 minutes', timestamp) is SQL-native. Combine with standard aggregates and tag GROUP BY for chart-ready output.

  • Materialised views (incremental refresh)

    Continuous aggregates over the source series. The engine refreshes them at commit time as new chunks land — dashboards read the view, not raw points, so KPI queries resolve in milliseconds.

  • Partition TTL

    Per-chunk TTL overrides on CREATE HYPERTABLE. A noisy metric can have 7 days; a billing usage series can have 13 months. Same series, per-partition policies.

  • High-cardinality tag indexing

    Tag tuples ({host, region, service}) are indexed automatically. Queries filtering by host = ? or region = ? are sub-linear even at 10 M unique combinations.

  • Joinable to relational tables

    JOIN users u ON u.id = m.tags->>'user_id' works directly. The planner picks the cheaper side — you do not stage time-series data into a sidecar warehouse to enrich it.

  • Eventual consistency reducers

    High-throughput counter time-series (request counts, error tallies) can use EC Sum reducers — append to the txn log without a write lock, consolidate periodically. Lower latency at the cost of a read-staleness window.

  • Policies scoped by tag

    A policy can constrain reads to specific tag dimensions — Allow: tags.tenant_id = ${claim.tenant_id} is a one-line tenant isolation rule across every metric query.

  • Read replicas

    Time-series queries are read-heavy and bursty (dashboards, scheduled reports). Read replicas off-load aggregate scans from the primary; the planner routes reads when replica lag is below the configured threshold.

  • PITR via WAL archive

    Point-in-time recovery walks the WAL backwards to any wall-clock timestamp. Useful for "show the metrics state from before the bad deploy" forensics.

Showcases

What time-series can do once you bring the rest of the engine in.

Cross-model correlation, algorithm flex, multi-model patterns — things that take a stack of services elsewhere.

Per-tenant SLA monitoring

Latency time-series joined to a tenants table — alerts when any single tenant breaches.

A request-latency time-series tagged with tenant_id joins to the relational tenants table to pick up the per-tenant SLA target. Any tenant whose 5-minute p99 breaches their own SLA fires an alert — without an external observability stack to maintain.

sql

WITH latency_5m AS (
  SELECT
    time_bucket('5 minutes', timestamp) AS bucket,
    tags->>'tenant_id'                  AS tenant_id,
    PERCENTILE_CONT(0.99)
      WITHIN GROUP (ORDER BY value)     AS p99
  FROM metrics
  WHERE metric = 'request.latency'
    AND timestamp > now() - INTERVAL '15 minutes'
  GROUP BY bucket, tenant_id
)
SELECT t.name, l.bucket, l.p99, t.sla_p99_ms
FROM latency_5m l
JOIN tenants t ON t.id::text = l.tenant_id
WHERE l.p99 > t.sla_p99_ms
ORDER BY l.bucket DESC, l.p99 DESC;

Live billing rollup powered by a continuous aggregate

Per-customer monthly usage updated incrementally as raw points land.

A billing_usage time-series feeds a materialised view bucketed by day. The invoicing job sums daily buckets into a calendar month — never scans the raw points. The view refreshes itself at commit time as new chunks land; you don't run a refresh cron.

sql

CREATE MATERIALIZED VIEW mv_billing_daily AS
  SELECT
    time_bucket('1 day', timestamp) AS day,
    tags->>'org_id'                 AS org_id,
    metric_kind,
    SUM(quantity)                   AS total
  FROM billing_usage
  GROUP BY day, org_id, metric_kind;

-- Monthly invoice query (sub-millisecond — reads from the view).
SELECT org_id, metric_kind, SUM(total) AS month_total
FROM mv_billing_daily
WHERE day >= date_trunc('month', now())
GROUP BY org_id, metric_kind;

Migrate from

Bring time-series over from your current tool.

From

InfluxDB

Export Influx line-protocol via influx_inspect, transform to RedDB INSERT statements. Tags become a JSONB column.

influx_inspect export -database telegraf -out telegraf.txt
red import --timeseries metrics --line-protocol telegraf.txt \
  reds://admin@<db>.<org>.db.reddb.io:5050

From

TimescaleDB

Hypertables map to RedDB time-series 1:1. SELECT INTO via the wire protocol — no driver change needed.

INSERT INTO metrics (timestamp, metric, value, tags)
SELECT time, metric, value, tags::jsonb
FROM timescale_export.metrics_hypertable;

Recipes

Common patterns, real driver code.

sql

Latency p99 dashboard

Continuous aggregate over per-request latency points — 5-minute buckets the dashboard reads instead of raw rows.

CREATE MATERIALIZED VIEW mv_request_latency_5m AS
  SELECT
    time_bucket('5 minutes', timestamp) AS bucket,
    tags->>'route' AS route,
    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY value) AS p99_ms
  FROM metrics
  WHERE metric = 'request.latency'
  GROUP BY bucket, route;

sql

Per-customer billing usage

Monthly resource consumption rolled up at write time so the invoicing job is a single SELECT.

SELECT
  tags->>'org_id' AS org,
  metric,
  SUM(value) AS total
FROM billing_usage
WHERE timestamp >= date_trunc('month', now())
  AND timestamp <  date_trunc('month', now()) + INTERVAL '1 month'
GROUP BY org, metric
ORDER BY org, metric;

python

IoT telemetry write path

Bulk-insert via the gRPC driver — high-cardinality tags, no schema changes per device family.

from reddb import connect
import time

db = connect("reds://admin@.../my-db:5050")
batch = []
for reading in sensor.stream():
    batch.append({
        "timestamp": time.time_ns() // 1_000_000,
        "metric": "temperature.celsius",
        "value": reading.value,
        "tags": {"device": reading.device_id, "site": reading.site}
    })
    if len(batch) >= 5000:
        db.timeseries.insert("metrics", batch)
        batch.clear()

Limits

What it does. What it costs.

Operational caps and durability semantics, no hand-waving.

Bucket granularity
1 millisecond → 1 year (any time_bucket interval)
Retention
Engine-managed chunk drop, no row-by-row delete
Ingest throughput
~241k points/sec on commodity hardware (gRPC)
Tag cardinality
Tested up to 10 M unique tag tuples per series
Materialised views
Incrementally refreshed at commit time

When NOT to use

Don't pick time-series for these.

Honest constraints — when another model fits better.

  • Don't use time-series for OLTP rows

    A users / orgs table is not a time-series. Time-series chunks are append-only and pruned by retention; that is the wrong shape for entities that get updated.

  • Don't store text logs as time-series

    Body fields with full event JSON belong in a document collection — text + structured indexes give you free-text search. Time-series is for numeric points.

  • Avoid `tags` with unbounded user-defined fields

    Tag cardinality drives index size. Pre-validate the set of tag keys per collection so a runaway tag does not balloon storage.

vs

How RedDB time-series compare.

InfluxDB

Same time_bucket / retention model on an engine that joins to your other data. No Telegraf / sync hop.

TimescaleDB

Built-in retention + materialised views, same Postgres-wire compatibility, no extension to install.

FAQ

Questions teams ask before they pick time-series.

How fine-grained can the buckets be?

time_bucket accepts intervals from 1 millisecond up to years. Practical bucket sizes range from 1 second for live dashboards to 1 month for billing rollups.

Can I join time-series points to a table?

Yes. JOIN users u ON u.id = m.tags->>'user_id' works exactly as you would write it. The planner picks the cheaper side as the driver.

How does retention actually work?

The engine drops whole chunks once their max timestamp falls outside the retention window. No row-by-row delete, no vacuum cycles.

Are materialised views auto-refreshed?

Yes — incrementally. Each new chunk written to the source updates the dependent views in the same commit. No scheduled refresh job to operate.

High cardinality (millions of unique tag combinations) — does this hold up?

For the typical metric / observability workload, yes. We test up to 10 M unique tag tuples on commodity hardware. Past that, partition by tag dimension into separate time-series.

Try time-series on RedDB.

Free nano database, no credit card. AGPL self-host or managed Cloud — same engine, same file format.