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.
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}Measured 2026-07-27, pinned box · full tables and caveats
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.
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.
ldd $(which ubik-engine) linux-vdso.so.1 libstdc++.so.6 libm.so.6 libgcc_s.so.1 libc.so.6Two. 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.
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.
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.
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"}