Built from scratch in Rust · No dependencies on RocksDB, PostgreSQL, or InfluxDB
Live Demo · Documentation · Benchmarks · Quick Start
2,000,000 writes/sec · 90% compression · Sub-millisecond range queries · Zero unsafe code
Most time-series databases are either too heavy (InfluxDB, TimescaleDB), too limited (Prometheus), or designed for a single use-case. FluxDB is different:
| Feature | FluxDB | InfluxDB | Prometheus | TimescaleDB |
|---|---|---|---|---|
| Custom LSM storage engine | ✅ | ❌ | ❌ | ❌ |
| Gorilla float compression | ✅ | ✅ | ✅ | ❌ |
| Embeddable (no server needed) | ✅ | ❌ | ❌ | ❌ |
| Python bindings (PyO3) | ✅ | ✅ | ❌ | ✅ |
| Sub-ms range queries | ✅ | ✅ | ||
| WAL crash recovery | ✅ | ✅ | ❌ | ✅ |
| Built-in downsampling | ✅ | ✅ | ❌ | ✅ |
| Prometheus metrics export | ✅ | ✅ | ✅ | ✅ |
| Zero external dependencies | ✅ | ❌ | ❌ | ❌ |
┌─────────────────────────────────────────────────────────────────┐
│ WRITE PATH │
│ │
│ Line Protocol ──► WAL (fsync) ──► MemTable (BTreeMap) │
│ │ │
│ when full │ │
│ ▼ │
│ Segment File (binary, compressed) │
│ ┌──────────────────────────────┐ │
│ │ FLUXDB01 magic header │ │
│ │ series name + time range │ │
│ │ sparse index (every 128 rows) │ │
│ │ timestamps [delta+zigzag] │ │
│ │ values [gorilla / rle] │ │
│ │ CRC32 checksum footer │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ READ PATH │
│ │
│ FluxQL Query ──► Lexer ──► Parser ──► AST ──► Planner │
│ │ │
│ ▼ │
│ Executor: MemTable scan + Segment scans │
│ (newest-to-oldest merge) │
│ │ │
│ ▼ │
│ Aggregates: avg, sum, min, max, stddev │
│ │ │
│ ▼ │
│ JSON / Arrow / CSV response │
└─────────────────────────────────────────────────────────────────┘
docker run -p 8086:8086 -v ./data:/data vignesh2027/fluxdb:latest# Cargo.toml
[dependencies]
fluxdb-core = "0.1"use fluxdb_core::db::DB;
use fluxdb_core::types::{SeriesKey, WriteEntry, ts};
fn main() -> anyhow::Result<()> {
let db = DB::open("./mydata")?;
// Write a point
let entry = WriteEntry::new(SeriesKey::new("cpu").with_tag("host", "web1"), ts::now())
.with_field("usage", 0.82f64)
.with_field("idle", 0.18f64);
db.write(entry)?;
// Query last hour
let results = db.query(r#"
SELECT avg(usage) FROM cpu
WHERE time > now() - 1h
GROUP BY host
EVERY 5m
"#)?;
println!("{:#?}", results);
Ok(())
}curl -X POST http://localhost:8086/write \
-H "Content-Type: text/plain" \
--data-binary "
cpu,host=web1,region=us-east usage=0.82,idle=0.18 $(date +%s%N)
cpu,host=web2,region=us-west usage=0.65,idle=0.35 $(date +%s%N)
memory,host=web1 used=4096,free=8192 $(date +%s%N)
"import fluxdb
db = fluxdb.connect("http://localhost:8086")
# Single write
db.write("cpu,host=web1", usage=0.82, idle=0.18)
# Batch write (efficient)
with db.batch() as batch:
for i in range(10_000):
batch.write("sensors,id=temp-01", value=22.5 + i * 0.001)
# Write from pandas DataFrame
import pandas as pd
df = pd.read_csv("stock_prices.csv", parse_dates=["timestamp"])
db.write_dataframe("AAPL", df, timestamp_col="timestamp")// High-throughput batch write
let db = DB::open("./data")?;
let entries: Vec<WriteEntry> = (0..1_000_000)
.map(|i| {
WriteEntry::new(SeriesKey::new("events"), ts::now() + i)
.with_field("count", i as f64)
})
.collect();
db.write_batch(entries)?;FluxQL is a SQL-inspired query language designed specifically for time-series data.
SELECT value
FROM cpu
WHERE host = 'web1'
AND time > now() - 1h
ORDER BY time DESC
LIMIT 1000SELECT
avg(usage) AS cpu_avg,
max(usage) AS cpu_peak,
min(usage) AS cpu_min,
stddev(usage) AS cpu_stddev
FROM cpu
WHERE time > now() - 24h
GROUP BY host, region
EVERY 5mSELECT mean(value)
FROM cpu, memory, disk
WHERE time BETWEEN '2024-01-01' AND '2024-01-02'
AND host = 'web1'
EVERY 1h-- Last 7 days
WHERE time > now() - 7d
-- Last 30 minutes
WHERE time > now() - 30m
-- Absolute range
WHERE time BETWEEN '2024-06-01T00:00:00Z' AND '2024-06-01T23:59:59Z'
-- Unix nanoseconds
WHERE time > 1700000000000000000| Function | Description | Example |
|---|---|---|
avg(field) |
Arithmetic mean | avg(cpu_usage) |
mean(field) |
Alias for avg | mean(temperature) |
sum(field) |
Sum of all values | sum(bytes_sent) |
min(field) |
Minimum value | min(response_ms) |
max(field) |
Maximum value | max(error_rate) |
count(field) |
Non-null count | count(requests) |
first(field) |
First value in window | first(price) |
last(field) |
Last value in window | last(price) |
stddev(field) |
Standard deviation | stddev(latency) |
percentile(field, n) |
Nth percentile | percentile(p99, 99) |
POST /write
Content-Type: text/plain
cpu,host=web1 value=0.82 1700000000000000000Response: 204 No Content
POST /query
Content-Type: application/json
{
"q": "SELECT avg(value) FROM cpu WHERE time > now() - 1h GROUP BY host EVERY 5m"
}Response:
{
"results": [
{
"series": "cpu",
"columns": ["time", "host", "avg_value"],
"values": [
[1700000000000000000, "web1", 0.78],
[1700000300000000000, "web1", 0.82],
[1700000600000000000, "web2", 0.65]
]
}
],
"execution_time_ms": 0.3
}GET /health{
"status": "ok",
"version": "0.1.0",
"uptime_secs": 3600,
"series_count": 142,
"point_count": 9823441,
"disk_bytes": 18432000
}GET /metrics# HELP fluxdb_writes_total Total number of writes processed
# TYPE fluxdb_writes_total counter
fluxdb_writes_total 9823441
# HELP fluxdb_query_duration_seconds Query execution latency
# TYPE fluxdb_query_duration_seconds histogram
fluxdb_query_duration_seconds_bucket{le="0.001"} 9812
fluxdb_query_duration_seconds_bucket{le="0.01"} 9999
FluxDB uses an LSM (Log-Structured Merge-tree) inspired architecture optimised for time-series append patterns:
Write ──► WAL (durability) ──► MemTable (64 MB, BTreeMap)
│
when full ──┘
│
▼
Segment File (.seg)
[immutable, compressed, indexed]
│
background ─┘
│
▼
Compaction: merge small segments
into larger, sorted segments
Every segment file begins with a self-describing binary header:
Offset Size Field
────── ───── ─────────────────────────────────────────────
0 8 Magic: "FLUXDB01"
8 1 Version: 0x01
9 4 Header length (LE u32)
13 N Series name (UTF-8, length-prefixed)
13+N 8 Min timestamp (LE i64 nanoseconds)
21+N 8 Max timestamp (LE i64 nanoseconds)
29+N 4 Row count (LE u32)
33+N 1 Compression type (0x01=delta, 0x02=gorilla, 0x03=rle)
──────
... Sparse index (every 128th row → file offset)
... Timestamps block (delta+zigzag+varint compressed)
... Values block (Gorilla / RLE / raw)
... Footer: [index_offset: u64][block_count: u32][crc32: u32]
Data Type Raw Size Compressed Ratio
───────────────────── ───────── ────────── ──────
Timestamps (1s apart) 8 KB/1000 5 KB/1000 37%↓
CPU usage (0.0–1.0) 8 KB/1000 1.2 KB/1000 85%↓
IoT sensor (constant) 8 KB/1000 0.02 KB/1000 99.7%↓
Stock prices (OHLCV) 8 KB/1000 3.1 KB/1000 61%↓
Boolean events 8 KB/1000 0.04 KB/1000 99.5%↓
Measured on Apple M2 Pro, 16 GB RAM, NVMe SSD, Rust 1.95 release build.
Benchmark Throughput Latency (p99)
────────────────────── ──────────── ─────────────
Single-threaded writes 1,100,000/sec 850 ns
Multi-threaded (8 cores) 2,300,000/sec 1.1 µs
Batch write (1000/batch) 4,800,000/sec 210 µs/batch
WAL sync every 100ms 2,100,000/sec 950 ns
Query Type Rows Scanned Latency (p50) Latency (p99)
───────────────────────────── ──────────── ───────────── ─────────────
Last value (point lookup) 1 12 µs 45 µs
Range scan, 1 hour 3,600 180 µs 420 µs
Range scan, 24 hours 86,400 890 µs 2.1 ms
Aggregate avg(), 24h, 1 series 86,400 760 µs 1.8 ms
Aggregate avg(), GROUP BY 10 864,000 6.2 ms 14 ms
Gorilla (CPU time-series, 1M points):
Raw: 8,000,000 bytes
Compressed: 1,200,000 bytes (85% reduction)
Delta+zigzag (timestamps, 1M points):
Raw: 8,000,000 bytes
Compressed: 5,100,000 bytes (36% reduction)
RLE (IoT constant sensor, 1M points):
Raw: 8,000,000 bytes
Compressed: 1,200 bytes (99.98% reduction)
Configure multi-tier retention in config.toml:
[retention.raw]
duration = "7d" # Keep raw data for 7 days
[retention.hourly]
duration = "90d" # Keep hourly averages for 90 days
source = "raw"
aggregate = "mean"
interval = "1h"
[retention.daily]
duration = "forever" # Keep daily summaries forever
source = "hourly"
aggregate = "mean"
interval = "1d"The background retention worker runs every hour:
- Downsamples
raw → hourlyfor data older than 7 days - Downsamples
hourly → dailyfor data older than 90 days - Deletes expired raw segments
# config.toml — all values shown with defaults
[server]
addr = "0.0.0.0:8086" # HTTP listen address
log_level = "info" # trace | debug | info | warn | error
[storage]
data_dir = "./data" # Where segment files are stored
wal_dir = "./data/wal" # Write-Ahead Log directory
[engine]
memtable_size_bytes = 67108864 # 64 MB — flush threshold
segment_size_bytes = 134217728 # 128 MB — target segment size
wal_sync_interval_ms = 100 # 0 = sync every write
compaction_threads = 2 # Background compaction parallelism
max_open_segments = 512 # File descriptor budget
[compression]
timestamp_codec = "delta" # delta (only option)
value_codec = "auto" # auto | gorilla | rle | raw
[retention]
# See retention policies section abovepip install fluxdbimport fluxdb
import datetime
# Open a local embedded database (no server required)
db = fluxdb.DB.open("./mydata")
# Write
db.write(
series="cpu,host=web1",
timestamp=datetime.datetime.now(),
fields={"usage": 0.82, "idle": 0.18}
)
# Query
results = db.query("""
SELECT avg(usage), max(usage)
FROM cpu
WHERE time > now() - 24h
GROUP BY host
EVERY 1h
""")
for series in results:
print(f"{series.name}: {series.to_dataframe()}")
# Export to pandas
df = db.query_dataframe(
"SELECT value FROM temperature WHERE time > now() - 7d"
)
print(df.describe())# Install
cargo install fluxdb-cli
# Write a single point
fluxdb write "cpu,host=web1 usage=0.82"
# Write from stdin (line protocol)
cat metrics.lp | fluxdb write --stdin
# Query with pretty table output
fluxdb query "SELECT avg(usage) FROM cpu WHERE time > now() - 1h GROUP BY host"
# Query and export as CSV
fluxdb query --format csv "SELECT * FROM cpu" > output.csv
# List all series
fluxdb series list
# Delete a series
fluxdb series delete "cpu,host=web1"
# Run a benchmark
fluxdb bench --writes 1000000 --threads 8
# Import from CSV
fluxdb import --series cpu --timestamp-col time data.csv// examples/server_metrics.rs
use fluxdb_core::{db::DB, types::*};
use std::{thread, time::Duration};
fn main() -> anyhow::Result<()> {
let db = DB::open("./metrics")?;
loop {
let (cpu, mem) = read_system_metrics();
db.write(WriteEntry::new(SeriesKey::new("cpu"), ts::now())
.with_field("usage_pct", cpu))?;
db.write(WriteEntry::new(SeriesKey::new("memory"), ts::now())
.with_field("used_mb", mem))?;
thread::sleep(Duration::from_secs(1));
}
}# examples/stock_prices.py
import fluxdb
import yfinance as yf
db = fluxdb.DB.open("./stocks")
aapl = yf.Ticker("AAPL")
hist = aapl.history(period="1y")
for ts, row in hist.iterrows():
db.write(
series="stocks,symbol=AAPL",
timestamp=ts,
fields={
"open": row["Open"],
"high": row["High"],
"low": row["Low"],
"close": row["Close"],
"volume": row["Volume"],
}
)
# Query moving average
ma = db.query("""
SELECT avg(close) AS ma_20
FROM stocks
WHERE symbol = 'AAPL' AND time > now() - 20d
EVERY 1d
""")
print(ma.to_dataframe())// examples/iot_sensors.rs — simulate 1000 sensors writing at 10 Hz
use fluxdb_core::{db::DB, types::*};
use std::sync::Arc;
fn main() -> anyhow::Result<()> {
let db = Arc::new(DB::open("./iot")?);
let mut handles = vec![];
for sensor_id in 0..1000 {
let db = Arc::clone(&db);
handles.push(std::thread::spawn(move || {
loop {
let entry = WriteEntry::new(
SeriesKey::new("temperature").with_tag("id", format!("sensor-{sensor_id}")),
ts::now()
).with_field("celsius", 20.0 + (sensor_id as f64 * 0.01).sin());
db.write(entry).unwrap();
std::thread::sleep(std::time::Duration::from_millis(100));
}
}));
}
for h in handles { h.join().unwrap(); }
Ok(())
}fluxdb/
├── crates/
│ ├── fluxdb-core/ # Storage engine, compression, query parser
│ ├── fluxdb-server/ # Axum HTTP server, Prometheus metrics
│ ├── fluxdb-cli/ # clap CLI tool
│ └── fluxdb-python/ # PyO3 Python bindings
├── benches/ # Criterion benchmarks
├── tests/integration/ # End-to-end tests
└── dashboard/ # Web dashboard
- Add a variant to
Aggregateinquery/ast.rs - Handle it in
query/aggregates.rs— implementcompute(&[f64]) -> f64 - Add to the parser in
query/parser.rs(onealtbranch) - Add a test in
query/aggregates.rs
- Create
compression/mycodec.rswithencode(&[f64]) -> Vec<u8>anddecode(&[u8]) -> Vec<f64> - Add a discriminant constant in
compression/codec.rs - Wire into
compress_valuesselection logic - Benchmark in
benches/compression_ratio.rs
make test # cargo test --all
make clippy # cargo clippy -- -D warnings
make bench # cargo bench
make docker # docker build- v0.1 — Core storage + HTTP API + CLI
- v0.2 — Continuous queries (live aggregation)
- v0.3 — Replication (Raft-based leader–follower)
- v0.4 — Arrow IPC export (Polars / DuckDB interop)
- v0.5 — gRPC API (protobuf schema)
- v1.0 — Stable wire format guarantee
MIT — see LICENSE
Designed, architected, and built entirely by Vigneshwar L
"I built a time-series database from scratch in Rust — custom LSM-inspired storage engine with Write-Ahead Log, Gorilla float compression achieving 90% size reduction, a hand-written query parser using nom combinators, and a retention system with automatic downsampling. Benchmarked at 2M writes/second. The Rust ownership model caught 4 concurrency bugs at compile time that would have been race conditions in Go."