Benchmarks
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.
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, journaled work: 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). Thetoken-transfer-relayexample minus the balance-gate read. - Load: 30 qualifying deposits via
cast send; each produced exactly one run (n=30, all succeeded, zero duplicates). - Measurement: timestamps from 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; detection latency compares the event's block timestamp to the claim time. - Hardware: Apple M5 Max, 128 GB RAM (a development laptop).
- 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 methodology, and on-chain receipt verification per tx. Its ~32 ms p50 claim β send settled (dev build) is consistent with the table above.
Honest caveats
- Local RPC. anvil answers
eth_call/eth_estimateGasin microseconds; a real provider adds its round-trips to the ~25 ms (two RPC calls sit inside the measured window). The number isolates rflow's own overhead. - Detection is poll-bound in this setup. The 0.83 s p50 reflects block
polling against a 1-second chain with 1-second timestamp granularity.
Tightening
block_poll_frequency(networks config) shrinks it; on real chains the block interval (12 s on mainnet) dominates.ws:is parsed but not wired to the engines yet, so it does not help here. - Queued, not confirmed. The relayer owns broadcast, gas bidding and
inclusion. End-to-end "event β CONFIRMED" is chain-time dominated (block
interval Γ your
confirmationsdepth). - One machine, one process, modest n. n=30 on a dev laptop is a
smoke-level benchmark, not a load test. Concurrency limits
(
max_concurrent_runs, group lanes) were not stressed.
Why no competitor comparison table?
Tenderly Web3 Actions and Gelato are hosted: their eventβaction latency includes detection infrastructure, queueing and multi-tenant scheduling that cannot be benchmarked in a controlled way. OpenZeppelin's Relayer + Monitor pair runs locally, but its workflow layer is whatever glue you write between the two services (Monitor webhook β your code β Relayer HTTP API), so any number would benchmark that glue. 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).