Quick Start

Five minutes, one binary. You’ll create a database, define a stream of API latencies, load ten thousand readings, ask for their percentiles, roll them up, take a snapshot and ask again in SQL. Every command below was run against the released binary before this page went up.

1. Get the Binary

Download the archive for your system from the download page, unpack it and check it runs:

rollupdb version

2. Say What to Keep

A stream definition is the contract: which dimensions to group by, which statistics to keep, and how long each resolution lives. Save this as latency.json:

{
  "name": "latency",
  "version": 1,
  "effective_at": "2026-01-01T00:00:00Z",
  "dimensions": ["service"],
  "statistics": ["count", "sum", "min", "max", "avg", "variance_sample"],
  "max_groups": 100,
  "tiers": [
    {"width_seconds": 1,     "retain_seconds": 3600,     "grace_seconds": 120},
    {"width_seconds": 60,    "retain_seconds": 604800,   "grace_seconds": 120},
    {"width_seconds": 3600,  "retain_seconds": 31536000, "grace_seconds": 120},
    {"width_seconds": 86400, "retain_seconds": 0,        "grace_seconds": 120}
  ],
  "sketch": {"algorithm": "kll_float64", "encoding_version": 1, "k": 200, "m": 8}
}

That’s seconds for an hour, minutes for a week, hours for a year and days for good (0 means forever). Readings may arrive up to two minutes late (grace_seconds). The sketch line adds percentiles. Then:

rollupdb init metrics.rollupdb
rollupdb create-stream metrics.rollupdb latency.json

3. Load Some Readings

Readings are one JSON object per line. This makes ten thousand latencies for two services, timestamped a second ago, and a query for the web service’s slice:

python3 - <<'PY'
import datetime, json, random
now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
at = (now - datetime.timedelta(seconds=1)).isoformat().replace("+00:00", "Z")
with open("readings.ndjson", "w") as f:
    for i in range(10000):
        service = "web" if i % 2 else "api"
        ms = round(random.lognormvariate(3, 0.6), 2)
        f.write(json.dumps({"time": at, "value": ms, "dimensions": {"service": service}}) + "\n")
with open("query.json", "w") as f:
    json.dump({"stream": "latency", "version": 1, "from": at,
               "to": now.isoformat().replace("+00:00", "Z"),
               "filter": {"service": "web"}, "quantiles": [0.5, 0.95, 0.99]}, f)
PY
rollupdb ingest-ndjson metrics.rollupdb latency readings.ndjson

Each batch prints a receipt after it’s committed, and the last line sums it up:

{"type":"complete","committed_records":10000,"committed_batches":10,"accepted":10000,"rejected_invalid":0,"rejected_late":0,"rejected_group_limit":0,"next_line":10001,"discarded_records":0,"unacknowledged_records":0}

4. Ask

rollupdb query metrics.rollupdb query.json

The answer has the exact statistics, the percentiles marked as approximate, and what the query actually covered. From the run behind this page:

"statistics": {"avg": 23.531772, "count": 5000, "max": 144.25, "min": 2.33,
               "sum": 117658.86, "variance_sample": 230.5165514503064},
"quantiles": [{"rank": 0.5,  "value": 19.95, "approximate": true},
              {"rank": 0.95, "value": 51.41, "approximate": true},
              {"rank": 0.99, "value": 77.65, "approximate": true}],
"complete": true,
"resolution_seconds": 1

Your numbers will differ, since the readings are random. quantile_accuracy in the same answer gives the sketch’s error model.

5. Roll Up, Back Up

rollupdb advance metrics.rollupdb latency
rollupdb stats metrics.rollupdb latency
rollupdb snapshot metrics.rollupdb backup.rollupdb

advance folds buckets that are old enough into the next tier and drops what’s past its retention; without a stream name it does every stream. Readings this fresh stay where they are until they age. You don’t have to remember to run it: rollupdb serve does it for every stream once a minute. snapshot writes a consistent copy to one file you can move anywhere. Don’t copy the live file while something has it open.

6. Or Over HTTP

rollupdb serve metrics.rollupdb --listen 127.0.0.1:7070
curl -H 'Content-Type: application/json' --data-binary @query.json http://127.0.0.1:7070/v1/query

The service listens on loopback only, takes the same JSON and rolls up every stream once a minute while it runs (--advance-every changes that).

7. Or in SQL

Each stream is also a SQL view, one row per bucket:

rollupdb sql metrics.rollupdb "SELECT service, sum(count) AS readings,
  round(sum(sum) / sum(count), 2) AS mean_ms, max(max) AS worst_ms,
  round(rollup_quantile(sketch, 0.99), 2) AS p99_ms
  FROM latency GROUP BY service ORDER BY service"
{"columns":["service","readings","mean_ms","worst_ms","p99_ms"],
 "rows":[["api",5000,23.85,165.13,81.23],["web",5000,23.84,168.76,77.56]],
 "truncated":false}

rollup_quantile merges the buckets’ sketches, so the p99 is right across any rows you pick. SQL here is read-only: a DELETE gets refused. The reference lists every command, endpoint and SQL function.