Benchmarks
rflow's performance claim is architectural: event decode β expression eval β relayer hand-off are in-process function calls β no HTTP hop, no serialization on the hot path. This page puts numbers on that, and is precise about what was measured and what was not.
Headline numbers
The engine-side hot path β event claimed β transaction queued in the embedded relayer β measured over 30 event-triggered sends:
| Metric | p50 | p95 | max |
|---|---|---|---|
| Claim β tx queued (simulate + estimate gas + policy checks + relayer hand-off) | 25.1 ms | 34.3 ms | 39.2 ms |
Claim β run settled (full run, single send step, wait_for: none) | 28.1 ms | 37.3 ms | β |
| Event's block timestamp β claim (detection, polling a 1s-block chain) | 0.83 s | 0.91 s | 0.92 s |
Those ~25 ms include real work, all of it journaled: the exactly-once claim
transaction in Postgres, template evaluation, a pre-flight eth_call
simulation, an eth_estimateGas, policy/gas-cap checks, the pre-send journal
write with the idempotency key, and the in-process relayer queue insert.
Methodology β reproduce it yourself
- Setup: a release build of
rflow(v0.1.0,cargo build --release), a localanvilchain (--block-time 1, chain id 31337) and the repo's compose Postgres, all on one machine. - Workflow: an ERC20
Transferevent trigger with awhere:filter β onesend_transactionstep (wait_for: none, simulation on β the default). Essentially thetoken-transfer-relayexample minus the balance-gate read. - Load: 30 qualifying deposits fired via
cast send; every one produced exactly one run (n=30, all succeeded, zero duplicates). - Measurement: timestamps are the engine's own journal β
workflow_runs.created_at(the claim) tostep_runs.finished_atof the send step, which withwait_for: nonesettles at the relayer queue ack. Percentiles computed in SQL over the journal; detection latency compares the event's block timestamp to the claim time. - Hardware: Apple M5 Max, 128 GB RAM (a development laptop, not a tuned server).
- In-repo harness:
cargo run -p rflow_e2e_tests -- --benchruns a self-contained variant (20 sequential deposits, instant-mining anvil, unoptimized dev build) and writesbench-results.jsonwith every sample, the full methodology, and on-chain receipt verification for each tx. Its numbers (~32 ms p50 claim β send settled on a dev build) are consistent with the release-build table above.
Honest caveats
- Local RPC. anvil answers
eth_call/eth_estimateGasin microseconds; a real provider adds its network round-trips to the ~25 ms (two RPC calls sit inside the measured window). The number isolates rflow's overhead β it is the part of the stack rflow can promise. - Detection is poll-bound in this setup. The 0.83 s p50 from block
timestamp to claim reflects the indexer's block polling against a 1-second
chain, and block timestamps have 1-second granularity. Tightening
block_poll_frequency(networks config) shrinks it; on real chains the block interval (12 s on mainnet) dwarfs this component entirely. (ws:is parsed but not wired to the engines yet, so it does not help here.) - Queued, not confirmed. The relayer owns everything after the queue β
broadcast, gas bidding, inclusion. End-to-end "event β CONFIRMED" is
chain-time dominated (block interval Γ your
confirmationsdepth) and would say nothing about the engine. - One machine, one process, modest n. n=30 on a dev laptop is a smoke-level
benchmark for the hot path, not a load test. Concurrency limits
(
max_concurrent_runs, group lanes) were not stressed here.
Why no competitor comparison table?
The obvious candidates (Defender, Tenderly Web3 Actions, Gelato) are hosted services in a different category: their eventβaction latency includes their detection infrastructure, queueing and multi-tenant scheduling, none of which is publicly benchmarkable in a controlled way β numbers we could publish would be unfair in one direction or the other. The architectural difference stands on its own: rflow's trigger-to-relayer path is a function call inside one process on your hardware, plus two RPC round-trips to your provider.
Where throughput actually goes
For capacity planning, the bottlenecks in practice, in order:
- Your RPC provider β simulation + gas estimation are two calls per send;
backfills are
eth_getLogs-bound (max_block_range, CU budgets). - Postgres β every claim, step and settle is a journaled write. Give it real storage; it is the durability you are paying for.
- The chain itself β inclusion and confirmation depth; rflow just waits well (durably parked, not spinning).