Build1 publisher3 min readPublished
Incremental accumulators keep rekuiper's window memory under 10 MB on one core
I-Dacs Labs rebuilt LF Edge eKuiper in Rust around a windowed path that stores no rows, and it reports 5 to 10 MB of memory on the workloads where the Go engines it tested climbed toward the 1 GB limit of the box.
The Engineer · Build desk

What happened
- I-Dacs Labs rebuilt LF Edge eKuiper in Rust and ran it against eKuiper, Telegraf and Redpanda Connect on five MQTT workloads, on one core with 1 GB of memory, checking output message by message.
- The Rust engine, rekuiper, produced complete and correct output at 100,000 messages per second on one core in every one of the five workloads, the top of the range the team tested.
- On the windowed workloads its memory stayed between 5 and 10 MB, while the Go-based engines climbed to between half a gigabyte and a full gigabyte, or failed.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint The bounded-memory result is scoped to group columns and count, sum, avg, min and max over simple expressions, so a rule using anything else has to be measured on your own burst traffic before the 5 to 10 MB figure means anything to you.
- exposure Reserve-then-commit rejects a batch outright when any subscriber queue is full, so the burst that used to inflate the engine's heap now accumulates in the MQTT broker and in device-side buffers instead.
- decision With the REST API, SQL dialect and CLI reimplemented, a trial costs a binary swap against rules you already run, and the decision reduces to how much weight you put on one team's self-published numbers.
- capability Because 100,000 messages per second was the harness limit and not a measured saturation point, single-core headroom for edge deployments is at least that figure and unknown above it.
Buffering rows until a window triggers makes memory a function of messages per window [5]. A fleet that reconnects together drives exactly that count up, along with chargers starting sessions at the same moment and devices flushing buffered readings after an outage [15]. rekuiper keeps one accumulator per group per aggregate and never stores the rows, so window memory becomes a function of the number of devices [4]. Before adopting that, you count distinct group keys, not messages per second. The post lists the shape the incremental evaluator covers: group columns, plain columns, and count, sum, avg, min and max over simple expressions [6].
Two defaults set the in-flight ceiling. Each subscriber queue on the in-process stream bus holds 4,096 records, and each rule's output drains through a sink queue that defaults to 10,000 records with a dedicated worker [7][8]. One rule path therefore tops out at 14,096 records in flight, and your record size converts that into bytes [1]. Admission is reserve-then-commit: a batch reserves capacity in every subscriber's queue and only then commits, so it is either delivered to all subscribers or rejected whole [7].
The MQTT source uses the rumqttc client, and when one network read surfaces several publishes it admits everything already buffered as a single batch of up to 1,024 records [9]. At 100,000 messages a second, full batches mean roughly 98 admissions per second where per-message handling would mean 100,000 scheduling events [2]. A maximal batch is a quarter of one subscriber queue [4]. I-Dacs Labs attributes about half the CPU per message of the Go engines on the simple workloads partly to this batching [10].
The reported memory gap runs from 50x to 200x depending on which ends you pair [3]. The rig was one core and 1 GB [1]. A Go engine sitting at a full gigabyte was against the limit of that box, and the post says some runs failed instead of climbing [3]. For the figures to transfer to your gateway, three things have to hold: your rules sit inside the covered aggregate set, your group cardinality is bounded by device count, and your bursts arrive as multi-publish network reads, because the batch admission depends on several publishes surfacing per read [6][4][9].
Engine, benchmark and the argument about what to measure all come from the same team, published on dev.to by I-Dacs Labs Engineering [18]. The post does not report an independent replication. The harness discipline is the part I would copy: output checked message by message, which found a correctness bug in their own code that a throughput-only test would have rewarded as "fast" [13].
Compatibility is claimed at 98 REST paths and 140 operations, checked black-box against eKuiper's own OpenAPI description [11], with the SQL dialect covering JSON paths, CASE, array indexing and unnest, and the stream options keeping names like DATASOURCE, FORMAT, CONF_KEY, SCHEMAID and TIMESTAMP [12]. The stated goal was "boring on purpose", so existing eKuiper rules, the eKuiper Manager web UI and deployment tooling keep working [17]. Against that, the post calls peak throughput on a big server "close to useless" for the hardware these engines run on, where you get one or two cores and a few hundred megabytes free [14].
What to watch
- An independent run of the same five MQTT workloads under the same one-core, 1 GB cap, by someone who did not write the engine.
- A published run above 100,000 messages per second, which would replace a tested-range figure with a measured ceiling.
- Whether the Go eKuiper project adopts incremental window aggregation upstream, which would close most of the reported memory gap without a language change.