Measured

Latency, checkpoint cost, throughput

A latency distribution, the cost of a checkpoint as state grows, and a throughput table per workload shape.

Two emit modes: latency, or per-window atomic visibility

Every engine byte-identical to the DuckDB oracle first, then measured back to back on one pinned box. Ubik ships two Kafka emit modes. Transactional wraps each window in a Kafka transaction, so a read_committed consumer sees it all-or-nothing. Idempotent drops the transaction and keeps exactly-once through the (window, group) upsert key and a durability barrier: the fast path.

1 ms101001 sUbikidempotent10.2 msProtondefault delivery11.9 msUbiktransactional33.1 msFlinkdefault delivery212.5 msSparkmicro-batch595.9 msRisingWaveemit on close2646.9 ms
Ubik idempotent Ubik transactionalp50, log scale. Every engine byte-identical to the DuckDB oracle first.

Idempotent lands a median of 10.2 ms, under Proton and one to two orders of magnitude under the cluster engines, in one process at 41 MB. The default transactional mode is second to Proton by design, the price of atomic visibility, and it stays the default.

EngineModep50p90p99Footprint
Ubikidempotent10.214.418.641 MB / 1 proc
Protondefault delivery11.913.113.5203 MB / 4 proc
Ubiktransactional33.145.950.541 MB / 1 proc
Flinkdefault delivery212.5258.9328.81406 MB / 6 proc
Sparkmicro-batch595.9785.2969.91357 MB / 6 proc
RisingWaveemit on close2646.92655.52666.6528 MB / 4 proc

The unit is milliseconds. Ubik carries exactly-once in both modes and a single process; the tail on the transactional row is the transaction commit round-trip.

Same box and session, cardinality dialled down to 100 keys: a whole window's emission lands inside a millisecond and the microsecond-resolution dump resolves the floor. Idempotent runs sub-millisecond at the fastest. This is a best-case floor, not a production shape; the 10 000-key table above is the distribution to plan against.

Ubik modep50p90p99Fastest
idempotent1.152.193.320.65
transactional1627.527.911.2

Intel i9-11900K, clock pinned at 3.5 GHz (host-pin: SMT off, governor performance, turbo off). 2026-07-27. 50 000 events/s from a paced live producer, 10 000 keys, TUMBLE 5 seconds, one partition, 120 seconds per engine, the first (warmup) window excluded symmetrically. Every engine byte-identical to the DuckDB oracle on tumble and hop before any latency was trusted. Pinned and same-session, so these are cited figures. The default (transactional) mode is second to Proton by design, the price of per-window atomic visibility; idempotent drops the transaction and keeps exactly-once.

What a checkpoint costs as state grows

The distribution above was measured at ten thousand keys, where a checkpoint costs under eight milliseconds. It does not stay there. A snapshot writes about 72 bytes per open group at about 8.8 ms per MiB, linearly. If a million open groups is your cardinality, plan for a six hundred millisecond pause.

Open groupsState on diskCheckpoint pause
10 0000.69 MiB7.8 ms
100 0006.95 MiB61 ms
500 00034.8 MiB306.4 ms
993 19469.14 MiB605.5 ms

The pause sits on the driver loop, once per checkpoint. On the default cadence that is roughly every two hundred thousand rows.

Intel i9-11900K, 6 workers, powersave governor. 2026-07-25. The real windowed operator, bound by the real planner, snapshotted at maximum open state. Swept by group cardinality. The write is CPU-bound rather than disk-bound: the same state to tmpfs and to encrypted btrfs differ by 8%. The pause is the serializer, and it is being worked on.

Throughput, per workload shape

A mono-node ceiling is a number per shape. Stateless shapes run at millions to tens of millions of events/s, windowed aggregates in the hundreds of thousands to low millions. Sliding windows and the stream-stream interval join are the known low end: if that is the workload, measure it first. Measured on six pinned cores of one machine.

WorkloadEvents/sWhat saturates next
Filter and project, from a file15 006 047decode fans out across workers
SESSION window, gap-based4 824 104scales radix-by-key
Count, from a Kafka topic2 035 904this is what the source itself delivers
TUMBLE window, from a file1 352 510the windowed-aggregate operator
TUMBLE window, from a Kafka topic1 037 481the shape most jobs actually run
TUMBLE window, state spilled to NVMe833 539state larger than the memory budget
HOP window, sliding652 202the sliding-window emit; measure first if this is your workload
Interval join, two streams on a time band431 176band match plus retention over two streams, the heaviest shape; scales with workers

Intel i9-11900K, 6 of 8 physical cores pinned at 3.5 GHz (host-pin: SMT off, governor performance, boost off), 31 GiB RAM, Linux 7.0.9 (Fedora 44). 2026-07-31. 5M rows, 3 repetitions, best-of wall clock, a real Redpanda broker for the Kafka rows. Each figure is the committed baseline, and a same-box regression run over 5 percent fails the merge gate. Each row is the ceiling for one workload shape. Pinned to base clock with SMT and boost off, so each cell ran at a spread under 2.5 percent: measurements, not samples of whatever the boost algorithm decided. Lower absolute numbers than an unpinned box, higher trust.

Measure it on your machine

The ceilings above come from the pinned bench host. Your own number takes a minute: generate five million JSON events, aggregate them, time it. No broker, no config, and the parse is included. The transcript below is one current laptop; yours is the number that matters.

your machine
awk 'BEGIN{for(i=0;i<5000000;i++)printf("{\"merchant\":\"m%d\",\"amount\":%d}\n",i%100,i%500)}' > events.ndjson time ubik --from file://events.ndjson \    "SELECT merchant, count(*) AS n, sum(amount) AS total     FROM events GROUP BY merchant" {"merchant":"m98","n":50000,"total":14900000}{"merchant":"m99","n":50000,"total":14950000}real  0m5.9s