Over at I-Dacs Labs, we run high-throughput telemetry pipelines on edge devices (Raspberry Pis, Advantech gateways, and embedded x86/ARM boxes). We've been using LF Edge eKuiper for local stream processing (SQL filtering, sliding windows, and MQTT/Kafka sinks), but kept hitting the classic edge computing wall:
- JVM engines (Apache Flink): Incredible throughput, but require >1 GB RAM and take 20+ seconds to boot. Unusable on small industrial hardware.
- Go engines (Upstream eKuiper, Benthos, Telegraf): Much lighter, but continuous Stop-The-World GC sweeps introduced tail latency jitter. Worse, under burst sensor loads (10k–100k events/sec), Go channel buffer saturation led to silent packet loss. We decided to rewrite the entire engine in pure Rust: rekuiper (v0.421-beta, dual licensed under MIT / Apache-2.0). --- ### What We Built
-
Core Architecture: Lock-free stream bus (
StreamBus), Tokio async actors for rule execution, and bounded actor queues for sinks with zero runtime GC pauses. - 100% Drop-In Parity: Fully compatible with the existing eKuiper Manager Web UI, OpenAPI 3.0 schemas, and standard streaming SQL. Zero scaffolded stubs across all 98 REST endpoints.
- Footprint: 9.60 MB stripped static binary, ~6 – 8.2 MB idle RAM consumption.
- Sub-15ms Cold Boot: 12.5 – 14.5 ms internal daemon bootstrap; 123 ms end-to-end process-spawn-to-ready.
Empirical Head-to-Head Benchmarks (500,000 Records)
Rather than hand-waving estimates, we ran all engines head-to-head on the exact same Linux machine (WSL2 / Ubuntu x86_64) using an identical 500,000-record telemetry workload:
Pipeline: Parse 500k JSON events → Compute formula (temp * 1.8 + 32) → Filter (temp > 20.0) → Project (id, temp_f) → Sink
| Engine | Runtime | 500k Elapsed | Throughput | Data Drops | Memory |
|---|---|---|---|---|---|
| rekuiper (0.421) | Pure Rust | 1.176 s | 425,308 eps | 0 (0.0%) | ~8 MB |
| Apache Flink | Java / JVM | 2.144 s | 233,209 eps | 0 (0.0%) | ~1,022 MB |
| Telegraf | Go | 8.194 s | 61,019 eps | 0 (0.0%) | ~50 MB |
| Upstream Go eKuiper | Go | 11.290 s | 44,287 eps | 72,921 (14.6%) | ~45 MB |
| Redpanda Connect | Go | 19.236 s | 25,993 eps | 0 (0.0%) | ~38 MB |
Key Observations:
-
Channel Saturation in Go: Under sustained 500k burst ingestion, upstream Go eKuiper dropped 72,921 records (14.6% data loss) due to channel saturation (
buffer full, drop message).rekuiperprocessed all 500,000 events with 0 drops in 1.176s (9.6x faster). -
vs Apache Flink: Flink’s execution graph is fast (233k eps), but the JobManager + TaskManager JVM consumed over 1 GB of RAM.
rekuiperbeats it in single-core throughput while consuming 125x less RAM (< 8.2 MB).
3. Cold Boot Time: rekuiper boots internally in ~13 ms (123 ms OS spawn to socket ready), compared to 1.2s for Go eKuiper and 20s for Apache Flink.
Reproduce It in 2 Minutes
All reproduction scripts and Docker configs are in the repository. Anyone can run the whole suite:
git clone https://github.com/ankur-paan/rekuiper.git
cd rekuiper
./test/benchmark/run_all.sh
Top comments (3)
425k eps with an 8MB idle footprint on edge hardware is incredible work. Go’s GC stop-the-world jitter and channel buffer dropouts have burned so many industrial IoT pipelines, so moving to a lock-free
StreamBusin Rust is the exact right answer.Question regarding your backpressure design: under sustained burst loads where downstream sinks (like a flaky cellular MQTT connection or slow Kafka broker) stall, how does your Tokio actor queue handle backpressure without unbounded heap allocation? Are you dropping with a deterministic ring-buffer strategy, or propagating pushback upstream to throttle the ingestion source?
Also, are you planning to open-source the benchmark suite comparing against Benthos and eKuiper? Would love to run this on our embedded ARM gateway clusters!
Thanks! Flaky cellular sinks on edge hardware were actually our main headache that started this rewrite.
On backpressure, it’s a two-stage approach:
Upstream pushback first: Sinks use bounded Tokio mpsc channels. When an MQTT connection or Kafka broker stalls, the worker stops consuming, and the rule evaluator awaits sink.send().await. This suspends the evaluator task and pauses socket reads, pushing TCP backpressure upstream to the sender.
Deterministic ring buffer fallback: The internal StreamBus uses fixed-capacity ring buffers (tokio::broadcast). If a UDP or burst source keeps blasting data anyway, memory stays strictly capped at O(1)—we yield Lagged(n) and drop stale frames rather than letting the heap balloon and OOM-killing the gateway.
And yes, the full benchmark suite is already open-sourced in the repo under test/benchmark/ (scripts for rekuiper, eKuiper, Benthos, Telegraf, and Flink).
Would love to see how it runs on your ARM cluster—if you get around to testing it, drop your numbers in an issue or PR!
This is still in beta as we are trying to cover all use cases for general stable release and would love to incorporate any suggestion if you/your team comes up with.
That two-stage backpressure strategy is textbook perfection for edge hardware. Using cooperative
sink.send().awaitfor TCP window pushback while falling back totokio::broadcastwithLagged(n)strictly guarantees O(1) heap bounds so the kernel OOM-killer never takes down the node.One quick thought/suggestion for the beta: when
Lagged(n)does trigger on UDP/uncooperative bursts, do you expose an internal drop counter metric (e.g., via a Prometheus endpoint likerekuiper_dropped_events_totaltagged by stream/rule)? In industrial/SCADA setups, having deterministic drop auditability is huge for compliance when downstream networks degrade.Awesome that the benchmark suite is already under
test/benchmark/—grabbing the repo now to clone and star. Will run this against our ARM setup and definitely open an issue/PR with the telemetry profile. Excited to see this hit 1.0!