← 목록

rekuiper 0.500: Moving the Engine Hot Path to RAM and Finding its Exact Physical Limits (up to 200k msg/s)

devto 2026-09-22 원문 보기 ↗


When we published our earlier benchmarks for rekuiper (our Rust reimplementation of LF Edge eKuiper for edge gateways and IoT hubs), the numbers answered our first question: on five real-world MQTT workloads, memory stayed bounded between 5 and 10 MB while Go-based engines climbed to hundreds of megabytes or failed.

In that test, rekuiper sustained 100,000 messages per second on a single pinned core. At offered 200,000 msg/s, however, it dropped packets or backlogged upstream.

Saying "100k passed and 200k failed" left a massive 100,000 msg/s blind spot. Is the true ceiling 105k? 140k? 195k? And why did it choke at higher rates when the CPU wasn't fully maxed out on every workload?

When we profiled the bottleneck under burst loads, the culprit wasn't stream parsing or window math. It was the disk.

Even when an edge stream engine's data plane runs in memory, typical implementations still touch disk for metadata: checking SQLite tables for stream and rule definitions, resolving auth keys, reading configuration YAMLs, or persisting rule state changes. On a developer NVMe drive, SQLite lookups take microseconds. On an industrial gateway running off slow eMMC flash or a microSD card, a batch of flash writes causes I/O wait spikes that stall the Tokio runtime thread.

For v0.500-beta, we rebuilt the engine's internal catalog and hot paths around an in-memory Redis-style architecture, tuned our storage engine to be zero-contention, and then ran a hierarchical search with 1,000 msg/s resolution under a strictly bounded Mosquitto broker to find the exact physical limit of every workload.

Here is what we built, what we measured, and where single-core stream processing actually hits physical walls.

1. The Bottleneck: Why Disk Kills Edge Ingest

In eKuiper and similar edge software, metadata (streams, rules, schemas, auth) is stored in a key-value store or an embedded SQLite database.

Under moderate load (5k–20k msg/s), querying SQLite to validate a rule or look up a schema is unnoticeable. But when 100,000+ messages per second hit an edge machine:

  1. Lock Contention: When multiple concurrent connections or rules access SQLite simultaneously, lock acquisition overhead adds tail latency.
  2. Flash Storage Stalls: Embedded devices (Raspberry Pi, industrial gateways) do not have enterprise SSD write queues. If the engine writes checkpoint state or logs to flash while simultaneously servicing network buffers, the OS I/O scheduler blocks threads.
  3. Queue Backpressure: When a stream processor stalls for even 10 milliseconds waiting on disk, a 150k msg/s MQTT ingress generates a 1,500-message backlog in the socket buffer. Under QoS 0, Mosquitto drops those packets.

If we wanted to push edge ingestion beyond 100k msg/s on a single core, the hot path could not touch the filesystem at all.

2. What Changed in v0.500

Redis-Style In-Memory Catalog (MemoryCatalog)

We separated metadata persistence from metadata reads:

// crates/rekuiper-core/src/catalog.rs
pub struct MemoryCatalog {
    streams: parking_lot::RwLock<HashMap<String, StreamDefinition>>,
    tables: parking_lot::RwLock<HashMap<String, TableDefinition>>,
    rules: parking_lot::RwLock<HashMap<String, RuleDefinition>>,
}

Zero-Disk Hot Path & Caching

Channel Buffer Sizing

We expanded internal actor queue depths from 1,024 to 32,768 records. On a single pinned core, this provides enough buffer headroom to absorb operating system scheduling jitter without propagating backpressure back into the MQTT network loop.

3. Finding the Exact Limits: Hierarchical 1k Peak Search

Rather than testing arbitrary rounded rates, we searched for the exact ceiling of each workload using a hierarchical binary ladder:

Test Rig (Identical to previous benchmarks)

4. The Results: Exact Certified Ceilings

Here are the verified limits for all five workloads in rekuiper v0.500-beta:

Workload Scenario Certified Ceiling First Failure Limiting Factor Single-Core CPU Anon RAM
W1 Telemetry Filter (1,000 devices) 150,000 msg/s 151,000 msg/s CPU saturation (99.4%), broker drops 20.5% 94.5% 17.4 MB
W2 Device 10s Windows (1,000 devices) 200,000 msg/s 210,000 msg/s Generator schedule (engine lossless to 240k) 94.4% 6.7 MB
W3 ESPHome Topics (10,000 topics, meta(topic)) 150,000 msg/s 151,000 msg/s CPU saturation (99.3%), broker drops 5.0% 97.4% 16.6 MB
W4 Vehicle Windows (10,000 VIN wildcard topics) 200,000 msg/s 210,000 msg/s Generator schedule (engine lossless to 220k) 97.5% 18.1 MB
W5 EV Charger Sessions (SESSIONWINDOW(10, 2)) 126,000 msg/s 127,000 msg/s Session drain lag (16s exceeds 5s stability limit) 86.7% 6.0 MB

5. Workload Deep-Dive: What Broke Where

W1: Telemetry Filter (JSON parsing + condition)

W2: Per-Device 10-second Windows (1,000 devices)

W3: ESPHome Fleet (10,000 distinct topics, plain text)

W4: Vehicle Wildcard Aggregation (10,000 VIN topics)

W5: EV Charger Sessions (SESSIONWINDOW)

6. Comparison with Other Engines

Here is the updated head-to-head comparison on the common 5k–100k ladder:

Highest Verified Loss-Free Ingest Rate

Workload rekuiper 0.500 eKuiper 2.4.1 Telegraf 1.40.0 Redpanda Connect 4.109.0
W1: Telemetry filter 150k certified 20k 50k (backlog) 20k
W2: Device windows 200k certified 20k Lost 6–27% 5k
W3: ESPHome topics 150k certified 20k 50k (backlog) 20k
W4: Vehicle windows 200k certified 20k Inconsistent 5k
W5: Charger sessions 126k certified 20k Unsupported Unsupported

Memory at 20,000 msg/s (Engine Anonymous RAM)

Workload rekuiper 0.500 eKuiper 2.4.1 Telegraf 1.40.0 Redpanda Connect 4.109.0
W1: Telemetry filter 4.4 MB 15 MB 92 MB 72 MB
W2: Device windows 6.4 MB 536 MB 52 MB (lossy) 1,012 MB (crashed)
W3: ESPHome topics 4.5 MB 43 MB 85 MB 68 MB
W4: Vehicle windows 10.2 MB 886 MB 94 MB (lossy) 993 MB (crashed)
W5: Charger sessions 7.3 MB 832 MB Unsupported Unsupported

At 20k msg/s on 10,000 vehicle windows, engines that buffer raw rows in RAM consume 886 MB to 1 GB, risking kernel OOM kills on edge hardware. rekuiper maintains 10.2 MB by computing aggregates incrementally in fixed-size accumulators.

7. Real Sustained Score vs. Fake Buffer Backlog

One crucial lesson from this benchmark audit: eventual delivery is not throughput.

If an engine accepts 200,000 msg/s for 30 seconds by buffering everything into a huge internal queue, and then spends the next 45 seconds after the publisher stops draining that backlog, that is not a 200k engine. That is an engine surviving on buffer mercy.

In our benchmark harness:

  1. The publisher and broker queues are strictly bounded (Mosquitto queue capped at 4,096 messages).
  2. We record the upstream source gap at the exact millisecond publishing finishes.
  3. If more than 4,096 messages are backlogged at send-end, or if post-send drain takes longer than 5 seconds, the trial is marked as a failure, even if every single message is eventually written to disk.

When we claim 150,000 msg/s on W1 and W3, it means the engine processed all 4.5 million messages in real-time with an end-of-send backlog of 15 messages and a 1.0s drain time.

Trying It Out

rekuiper is free and open source under MIT / Apache-2.0:

  docker run -d --name rekuiper \
    -p 9081:9081 -p 20499:20499 \
    -e KUIPER__BASIC__CONSOLELOG=true \
    ankurkrp/rekuiper:0.500-beta

All raw evidence files, harness scripts (mqttgen, iotrunner), and reproduction instructions are published in test/benchmark/iiot-mqtt/.