Windowed Low Cardinality
Tumbling 30s event-time window grouped by region (16 fixed keys), aggregating count + avg(latency_ms). The tiny key set keeps state trivial, so this isolates framework + windowing overhead. Benched at each product's max sustainable single-partition rate (its knee).
Last updated: 2026-06-18 12:53 · same-machine, relative comparison.
Bench characteristics
In-process computation — Tumbling 30s event-time window, GROUP BY region (16 fixed keys) → count + avg(latency_ms); one pane per key per window close.
| product | delivery | processing | engine | partition parallelism | state |
|---|---|---|---|---|---|
| turbine | at-least-once | batch (Arrow columnar) | in-process · Rust core + free-threaded CPython 3.14t | intra-partition (3 shards, hash-keyed) | own state store (msgpack on disk) |
| flink | at-least-once | batch (pipelined, JVM code-gen SQL) | in-process · JVM (PyFlink local MiniCluster, parallelism=1) | none (one task slot / partition, parallelism=1) | heap (HashMapStateBackend) |
| quix | at-least-once | row (per-record StreamingDataFrame) | in-process · pure-Python over librdkafka (C client) | none (one consumer per partition) | RocksDB (embedded) |
| risingwave | at-least-once | batch (vectorized SQL) | external server · Rust (single-node: meta+compute+frontend) | none (one source reader per partition) | server-managed (Hummock LSM store) |
All products are benched at at-least-once (each one's default): Turbine, Quix, RisingWave and Flink can opt into exactly-once (Kafka transactions / a transactional sink) but the bench keeps the default, so the comparison is iso-semantics. Input is a single Kafka partition (lz4), so partition parallelism is what each product can extract from that one partition; windows are tumbling 30 s event-time.
Throughput / resource usage
| product | offered_in/s | consumed_in/s | output_rate | cpu_avg | cpu_p95 | rss_avg_mb | rss_max_mb | run |
|---|---|---|---|---|---|---|---|---|
| turbine 0.1.0a5 | 570,000 | 562,770 | 0.53 | 118.0 | 118.0 | 281.7 | 297.2 | 2026-06-17 22:06 |
| flink 2.2.1 | 650,000 | 640,716 | 0.53 | 117.6 | 126.6 | 1,182.8 | 1,189.8 | 2026-06-18 12:53 |
| quix 3.23.6 | 70,000 | 68,704 | 0.53 | 93.3 | 94.3 | 119.8 | 120.6 | 2026-06-16 07:42 |
| risingwave v2.8.4 | 300,000 | 296,897 | 0.53 | 96.8 | 101.2 | 1,344.8 | 1,355.8 | 2026-06-16 15:22 |
output_rate is the window-emission cadence (≈ keys ÷ window size), not a throughput figure — it only moves when windows close, which needs the consumer to keep up with offered_in/s. Compare CPU/RSS; see the README on benching windowed.
flink — PyFlink local MiniCluster, parallelism=1 (one task slot / one partition). Heap state backend (HashMapStateBackend), at-least-once checkpointing to the local filesystem. CPU/RSS are summed across the process tree INCLUDING the JVM child (GC/checkpoint/network threads), so CPU reads ~1 core (106-115%) even at parallelism=1 — the "one core" is the WORK parallelism (a single slot), not a CPU pinning. expected.output_rate is the fixed 16-region pane cadence (0.53). Single-partition knee ≈ 650k (consume ≈ offered, lag ≈ 0); 800k decouples (consume plateaus ~660k).
Sanity — are we benching the same thing?
| product | output_rate | expected | sample |
|---|---|---|---|
| turbine | 0.53 | 0.53 | 96 |
| flink | 0.53 | 0.53 | 80 |
| quix | 0.53 | 0.53 | 96 |
| risingwave | 0.53 | 0.53 | 96 |
Aggregated output: checked against the expected pane cadence (expected.output_rate in bench.yaml), not per-record selectivity.
Verdict: ✓ matches theory — output_rate within 2× of the expected pane cadence.