Reference
A short map of RollupDB 0.4.1. The full contracts, with every limit and edge case, are in the docs folder of the code repository.
Commands
| Command | What it does |
|---|---|
rollupdb version |
Prints the version |
rollupdb init FILE |
Creates a new database; never overwrites |
rollupdb create-stream FILE DEFINITION.json |
Defines a stream |
rollupdb update-stream FILE DEFINITION.json |
Adds the stream’s next version |
rollupdb stream FILE NAME |
Shows a stream’s definition |
rollupdb ingest FILE BATCH.json |
Ingests one batch of up to 10,000 readings |
rollupdb ingest-ndjson FILE STREAM INPUT |
Ingests one reading per line from a file or - for stdin, in committed batches (--batch-size, default 1000) |
rollupdb query FILE QUERY.json |
Answers a query |
rollupdb sql FILE STATEMENT |
Runs one read-only SQL statement, or - to read it from stdin |
rollupdb stats FILE NAME |
Counters, groups, buckets, coverage and tier frontiers |
rollupdb advance FILE [NAME] |
Rolls up buckets that are old enough and expires what’s past retention, for one stream or all of them |
rollupdb snapshot FILE DEST |
Writes a consistent copy to a new file |
rollupdb inspect FILE |
Format version, file ID and stream count |
rollupdb migrate FILE |
Upgrades a file from an older format; back it up first |
rollupdb serve FILE [--listen 127.0.0.1:7070] [--advance-every 1m] |
Runs the local HTTP service and rolls up every stream on a schedule |
Stream Definitions
| Field | Meaning |
|---|---|
name, version |
The stream’s name, and version 1 when it’s created; update-stream adds 2, then 3 |
effective_at |
Readings before this time are refused |
dimensions |
Up to 16 names to group by, such as service or device; every reading must carry all of them |
statistics |
Any of count, sum, min, max, avg, variance_population, variance_sample |
max_groups |
Cap on distinct dimension combinations; readings for a new one past the cap are refused and counted |
tiers |
From finest to coarsest: width_seconds, retain_seconds (0 is forever) and grace_seconds for late readings |
sketch |
Optional percentiles: {"algorithm": "kll_float64", "encoding_version": 1, "k": 200, "m": 8}, with k from 64 to 4096 |
Changing a Stream
update-stream adds the stream’s next version ("version": 2, then 3). A new version may change each tier’s retain_seconds and grace_seconds, max_groups and statistics, and it may add a sketch to a stream that has none. The name, effective_at, dimensions and tier widths stay, since the retained buckets were built with them; anything else is refused.
The change covers the whole history at once. Every bucket keeps its full exact state, so a statistic added later works on old data too. Shorter retention applies at the next rollup, and what’s already expired stays gone. A sketch added later covers buckets that start after the change; percentiles over older ranges come back as not_retained.
Readings and Queries
A reading is {"time": "...", "value": 12.5, "dimensions": {"service": "web"}}. Time is RFC 3339, the value is a finite number, and a reading later than its bucket’s grace window is refused and counted, never quietly merged somewhere else.
A query names a stream and a time range aligned to the finest tier, with an optional filter on dimensions and up to 32 quantiles as fractions (0.99, not 99). The answer says which resolutions it used, what it covered and what’s missing. Ranges that cut through a coarse bucket are refused rather than guessed.
Exact and Approximate
Count, sum, min, max and mean are exact, and count is a true integer. Variance is kept in a form that merges correctly and is accurate to floating-point limits. Percentiles come from a KLL sketch: approximate, with the error model returned in every answer. In the recorded test the worst observed rank error was 0.5%, against a model of 1.3%. The test results
Automatic Rollups
rollupdb serve rolls up every stream once a minute, and --advance-every changes that (0 turns it off). An embedding gets the same with db.RunRetention(ctx, every, report). advance FILE without a stream name, db.AdvanceAll and POST /v1/advance do one pass over every stream.
SQL
Each stream is a view named after it (or stream_ plus its name, if the name is taken by one of the file’s own tables), one row per retained bucket per group, with columns start, end, width_seconds, tier, one per dimension, count, sum, min, max, mean, m2 and sketch. Buckets never overlap, so sum(count), sum(sum), min(min) and max(max) are correct over any rows, and three aggregates merge the rest: rollup_variance(count, mean, m2), rollup_variance_sample(count, mean, m2) and rollup_quantile(sketch, rank).
SELECT service, substr(start, 1, 10) AS day,
sum(count) AS readings, sum(sum) / sum(count) AS mean_ms,
rollup_quantile(sketch, 0.99) AS p99_ms
FROM latency WHERE start >= '2026-10-01T00:00:00Z'
GROUP BY service, day ORDER BY service, day;
One statement at a time, starting with SELECT, WITH or VALUES, read-only, at most 10,000 rows. SQL sees what’s retained now; for coverage and gaps, use a query. The SQL notes
HTTP
rollupdb serve listens on a loopback address only, with no authentication; it’s for programs on the same machine.
| Method | Path | Body or result |
|---|---|---|
| GET | /v1/info |
Database info |
| POST | /v1/streams |
A definition; 201 when created |
| GET | /v1/streams |
Every stream’s name |
| GET | /v1/streams/NAME |
The current definition |
| GET, POST | /v1/streams/NAME/versions |
Every version; POST adds the next one |
| GET | /v1/streams/NAME/stats |
Counters, groups, buckets, coverage, frontiers |
| POST | /v1/ingest |
One batch; the result after commit |
| POST | /v1/query |
A query |
| POST | /v1/sql |
{"sql": "SELECT ..."}; columns and rows |
| POST | /v1/streams/NAME/advance |
{}; the rollup result |
| POST | /v1/advance |
{}; rolls up every stream |
| GET | /v1/snapshot |
Downloads a consistent copy |
POST bodies need Content-Type: application/json. Errors come back as {"error": {"code": "...", "message": "..."}}.
Go Library
db, err := rollupdb.Create(ctx, "metrics.rollupdb") // or rollupdb.Open
err = db.CreateStream(ctx, definition)
result, err := db.Ingest(ctx, batch)
answer, err := db.Query(ctx, query)
rolled, err := db.Advance(ctx, "latency")
all, err := db.AdvanceAll(ctx) // or db.RunRetention(ctx, time.Minute, nil)
rows, err := db.SQL(ctx, "SELECT sum(count) FROM latency")
err = db.UpdateStream(ctx, nextVersion)
err = db.Snapshot(ctx, "backup.rollupdb")
err = db.Close()
WithClock injects a clock for replay and tests. Use one owning process per file.
The Clock
RollupDB trusts the host clock to decide what’s late and what’s old enough to roll up. If the clock jumps more than 366 days past a stream’s last write, ingest and advance refuse with ErrClockJump (HTTP 409 clock_jump) and write nothing, so a wrong clock can’t expire history early. If a stream really sat idle that long, raise the limit once with ROLLUPDB_MAX_CLOCK_STEP (a Go duration such as 9000h, or 0 for none) or WithMaxClockStep.