11:20 AM PT · 20 min

Real-time features without streaming using RonDB

Mikael Ronström
Mikael RonströmHead of DataHopsworks AB

Online feature store model · today: shift left

A feature vector is a batch of key lookups

events jobstreaming pipelineprecompute aggregates transactionsRonDB table · customer_id merchantsRonDB table · merchant_id feature groupfeature group fraud_fvjoin on entity key feature view rdrsREST API serverbatch_feature_store modelfeature vectorper entity

The pipeline computes every aggregate before anyone asks for it.

A feature group is a RonDB table, keyed by entity key.

A feature view combines feature groups, joined on the entity key.

RDRS reads the Hopsworks metadata, then does a batched key lookup in the feature group tables.

RonDB benchmark · Graviton 4

22 benchmark clients · locust · 64 cpus each 36 rdrs servers · 16 cpus each 6 RonDB data nodes · 64 cpus each

Throughput optimized · batch size 150

104.5M

key lookups per second

15.6GByte/s of JSON delivered

Latency optimized · batch size 100 keys

61.4M

key lookups per second

2 msP99 latency

What if the aggregate ran when the request arrives?

No streaming pipeline. The latest events count.

Online feature store model · shift left vs shift right

shift left events jobstream pipelinerecompute, always aggregatesprecomputed read shift right events insert raw rowsTTL · ring buffer at requestaggregate queryfresh result

Shift left

  • Precompute all aggregates
  • Reduce latency of online operations
  • Requires a streaming pipeline
  • Requires continuous recompute of aggregates

Shift right

  • Compute aggregates as online aggregate queries
  • No need for streaming pipelines (reduced complexity)
  • Fresh aggregate results: take into account the latest events
  • React faster to a changing environment

Shift Right
• Removes the complexity of streaming pipelines
• Better Feature Freshness

Now let's look at how we achieve that.

Requirements · from shift right

The Online Feature Store now has to handle:

Complex join and aggregate operations
TTL feature
Ring Buffer feature
Map Spark SQL and DuckDB queries to RonDB queries

Generic feature queries, not just simple key lookups.

Example · four windowed features on one customer

12 h agonow Feature 1 · last 10 min · limit 10 Feature 2 · last 1 h · limit 50 Feature 3 · last 1 h · limit 200 Feature 4 · last 12 h · limit 500

Each tick is a row: new rows arrive at the right, at now, and move left as they age. Four features, four windows, four row limits.

Shift left: 4 different precomputed features, no feature freshness.

Shift right: keep the last 500 rows, at most 12 hours. 4 features, 4 SQL queries, always fresh.

TTL and ring buffer · in RonDB

customer_id 42 · ring buffer, limit 8 rows · ttl

Insert on arrival. RonDB does every delete.

Ring buffer: a limit on how many rows per entity key are stored. Overflow rows are overwritten.

TTL: a limit on how long rows stay visible. Old rows are purged; a minimum number of rows can be retained.

  • TTL only stores too many rows for active entities: every event within the TTL is kept, far more rows than the statistics need.
  • Ring Buffer only keeps too many rows for non-active entities: their last rows stay, long after they stopped being relevant.

The combination minimises the storage volume needed to get the proper statistics.

TTL and ring buffer · in RonDB

customer_id 42 · ring buffer, limit 8 rows · ttl

Together: rows older than 6 hours are purged, new rows fill the free slots, and once the ring is full the oldest row is overwritten.

RonSQL: the query goes to the data

Low-latency SQL for feature stores. Not a generic SQL engine.

RonSQL · pushdown

querySELECT … SUM() RonSQLparse, compileaggregation program RonDB data nodespartial aggregatepartial aggregatepartial aggregatepartial aggregatepartial aggregatepartial aggregate resultmerged, in parallel mysql · what RonSQL does not take

Supports joins, CTEs, aggregation. No cost optimizer: the feature store knows its data model.

Any query RonSQL accepts is pushed down to the RonDB data nodes.

The data nodes parallelize it automatically.

MySQL handles the rest, without a guarantee of pushdown.

RonSQL examples · 1 / 3

Two windowed features, one request

The last 10 transactions, and the last hour limited to 50. The two CTEs do not depend on each other, so RonDB computes them in parallel on the data nodes, each as an ordered scan of the primary key that stops at its limit. Two more CTEs aggregate them, and the main query joins their single rows.

parallel cteslimit in ctetime window
ronsql_cli sample data
ronsql_cli -e "
WITH last10 AS (
SELECT customer_id, event_time, amount
FROM transactions_1
WHERE customer_id = 42
ORDER BY event_time DESC
LIMIT 10),
last1h AS (
SELECT customer_id, event_time, amount
FROM transactions_1
WHERE customer_id = 42
AND event_time >= '2026-10-05 23:00:00'
ORDER BY event_time DESC
LIMIT 50),
a10 AS (SELECT COUNT(*) AS n_10, AVG(amount) AS avg_10
FROM last10),
a1h AS (SELECT COUNT(*) AS n_1h, AVG(amount) AS avg_1h
FROM last1h)
SELECT n_10, avg_10, n_1h, avg_1h
FROM a10, a1h;"
n_10 avg_10 n_1h avg_1h
10 186.420000 2 76.150000

RonSQL examples · 2 / 3

Snowflake schema: card to account to customer

Start from one card, walk two joins out to the account and the customer, return the features of each.

cte2 joinssnowflake
ronsql_cli sample data
ronsql_cli -e "
WITH `b` AS (SELECT `account_id`, COUNT(*) AS `hw_cnt`
FROM `cards_1`
WHERE `card_id` = 4711
GROUP BY `account_id`)
SELECT `j2`.`account_age_days` AS `acct_account_age_days`,
`j2`.`status` AS `acct_status`,
`j3`.`kyc_level` AS `cust_kyc_level`,
`j3`.`risk_segment` AS `cust_risk_segment`
FROM `b`
JOIN `accounts_1` AS `j2`
ON `j2`.`account_id` = `b`.`account_id`
JOIN `customers_1` AS `j3`
ON `j3`.`customer_id` = `j2`.`customer_id`;"
acct_account_age_days acct_status cust_kyc_level cust_risk_segment
1290 active 2 low

RonSQL examples · 3 / 3

Complex features: AVRO() decodes them

Arrays and structs are stored Avro-encoded. AVRO() decodes them with the feature store schema that the REST API server holds, so the query goes through RDRS.

avro()complex featurerest api
rdrs · /0.1.0/ronsql sample data
Q='
SELECT station_id,
AVRO(no2_hourly) AS no2_hourly
FROM station_measurements_1
WHERE station_id = 17;'
jq -n --arg q "$Q" \
'{database: "air_quality", query: $q}' |
curl -s --json @- "$RDRS/0.1.0/ronsql"
{"data":
[{"station_id":17,"no2_hourly":[31,28,35,42,39,37]}
]
}

Shift Right
• Removes the complexity of streaming pipelines
• Better Feature Freshness

The combination of RonSQL, TTL and Ring Buffer is what removes the complexity of streaming pipelines.

RonDB 26.10 · and the rest of the release

Scale

  • Up to 8191 API nodes
  • Up to 144 RonDB data nodes
  • RDMA as communication media
  • 2-level hashing for faster scans
  • Many small scaling improvements

Operations

  • Synchronized node restart
  • Rate limits per project and per user
  • Quotas per project
  • Deadlock detection algorithm
  • Memory booking subsystem
  • cgroup v1 in automatic thread configuration
  • Improved security functions

Interfaces

  • Vector scan searches, exact distance functions
  • Writes through the REST API
  • New RonDB client
  • Extended Redis support in Rondis
  • JIT compiler for the RonDB interpreter

RonDB · in production

22.10in production
25.10in production
26.02in production
26.10.0released with Hopsworks 5.5 (release candidate)
Multi-TByteclusters
8kcluster connections