Exactly-once

Streaming SQL
in one process

Counts you can put on an invoice, out of a Kafka topic, from one process on one machine. Real SQL, exactly-once checked row for row against a batch oracle on every merge, and a kill -9 that resumes byte-identical. Run it as a binary, or import it into your own process. No JobManager, no RocksDB tuning, no cluster.

Closed source. Free for personal, non-commercial and evaluation use.

live demo
ubik --from kafka://demo.getubik.dev/orders \  "SELECT merchant,          TUMBLE(ts, INTERVAL '5' SECOND) AS w,          count(*) AS orders   FROM orders   GROUP BY merchant, TUMBLE(ts, INTERVAL '5' SECOND)" {"merchant":"acme","w":"2026-08-04 20:02:15","orders":34}{"merchant":"globex","w":"2026-08-04 20:02:15","orders":31}
End-to-end latency
10.2 ms
P50, idempotent · P99 18.6 ms
Throughput, Tumble from Kafka
1 037 481 ev/s
the shape most jobs actually run
Checkpoint at 10 000 groups
7.8 ms
grows linearly with open state

Measured 2026-07-27, pinned box · full tables and caveats

Version 1.2.1One processExactly-onceByte-identical resume
Where a wrong count costs money

Metering, billing, quota, reconciliation

For alerting and dashboards an approximate count is fine, and plenty of free tools deliver one. Ubik is for the other workloads: metering, per-tenant quota, aggregation that feeds an invoice, where a double-counted window is a credit note.

The usual answer is a cluster: Flink on Kubernetes, a JobManager, TaskManagers, RocksDB tuning, an SRE who knows it. Most streaming jobs run well under 100k events/s, which fits on two cores. DuckDB made that argument for analytics; Ubik makes it for real-time.

No broker required

Four things you can check in sixty seconds

Everything below runs on a laptop with no Kafka, no container, and no config file. The last one talks to a live broker: ours.

One. It depends on nothing

libc, libstdc++, libm, libgcc, the whole list. DuckDB, librdkafka and OpenSSL are vendored and statically linked, so there is no JVM to size, no RocksDB to tune, and nothing to install next to it.

shell
ldd $(which ubik-engine)  linux-vdso.so.1  libstdc++.so.6  libm.so.6  libgcc_s.so.1  libc.so.6

Two. SQL over a file

Point it at newline-delimited JSON and the schema is inferred. The table name is the source basename. Results are JSON lines on stdout, so the next thing in your pipe already understands them.

first query
ubik --from file://events.ndjson \    "SELECT merchant, amount FROM events WHERE amount > 100" {"merchant":"acme","amount":250.0}{"merchant":"acme","amount":980.0}

Three. The same engine, in your process

Arrow in, Arrow out. A pandas or polars frame goes in as a lookup dimension, record batches come back. Same SQL, same operators, same guarantee, no server anywhere.

python
import ubik tbl = ubik.stream(    "SELECT merchant, count(*) AS c "    "FROM events "    "GROUP BY merchant, TUMBLE(event_time, INTERVAL 1 MINUTE)",    from_="file://events.ndjson",).df()

Four. A live stream to point it at

demo.getubik.dev carries two open read-only topics, orders and payments, correlated at the source. This is a stream-stream interval join over the public internet, against data moving right now. Unpaid orders never match; watch for the gaps in the ids.

live demo
ubik --from kafka://demo.getubik.dev/orders \    --stream payments=kafka://demo.getubik.dev/payments \  "SELECT o.order_id, o.merchant, p.status   FROM orders o JOIN payments p     ON o.order_id = p.order_id    AND p.ts BETWEEN o.ts AND o.ts + INTERVAL '10' SECOND" {"order_id":"o1785873397","merchant":"initech","status":"captured"}{"order_id":"o1785873398","merchant":"acme","status":"declined"}{"order_id":"o1785873400","merchant":"globex","status":"captured"}