Skip to content

Node.js messaging bench

This page records how the server-driven model of the specification (§10) is implemented in Node.js and how to run it. Comparison targets, patterns, units and judgement rules are owned by the specification; this page holds only the Node-specific values.

1. Comparison targets

Implementation request send/command API used
grpc-node BenchService.Echo unary (callback wrapped in a Promise) BenchService.Command unary, Empty reply @grpc/grpc-js, @grpc/proto-loader
zlink-node raw ROUTER socket.request(peer).message(...).timeout(...).submit() raw ROUTER socket.send(peer).message(...).submit() published @zlink-systems/zlink
zlink-framework-node framework channel request (planned) framework channel send (planned) @zlink-systems/framework, @zlink-systems/nestjs; every cell is unsupported today (§6)

gRPC and raw use the same BenchPayload protobuf body and the 29-byte header in front of it (spec §6). raw sends the framework envelope and the protobuf body as two parts.

2. How to run

# full matrix, always through the perf ticket queue
bash scripts/perf/perf-ticket.sh submit -p 1 -o <owner> -d "node with-grpc run" -- \
  bash framework/bench/grpc/node/run_local.sh

# one cell
env PAYLOADS=1024 SCENARIO=request-window IMPLEMENTATION=zlink-node \
  bash framework/bench/grpc/node/run_local.sh
Input Default Behaviour
RUNS 1 runs per runner process (measurement tickets are per run)
RUN_DEALER 0 only 0 is accepted under the comparison contract
DURATION 5 active window in seconds
WARMUP 1000 warmup calls before active
PAYLOADS 1024,4096 payload list; other values fail preflight
SCENARIO all all, request, or one of the four pattern names
IMPLEMENTATION all all or one of the three implementation names
WINDOW 100 request-window in-flight; other values fail preflight
STAMP run time run id and result path
OUTROOT framework/bench/grpc/log/node/with_grpc_node_<stamp> run root
SKIP_BUILD 0 1 skips npm ci and npm run build

Fixed: send concurrency 8, process ceiling 300 s, route/request/drain ceiling 30 s, settle quiet period 200 ms.

3. Process layout

Implementation source A A ports (trigger/stats) target B B ports
grpc-node client/main.js: one unary stub per logical stream 5220/5221 grpc-server/main.js: echo/count gRPC 5222, stats 5223
zlink-node client/main.js: one raw ROUTER for request or eight for send 5225/5226 zlink-raw-server/main.js: separate request and command ROUTERs request 5227, command 5228, stats 5229
zlink-framework-node not started (unsupported) 5232/5233 (reserved) zlink-framework-server/main.js RouteMesh 5234, stats 5235 (reserved)

The trigger, stats and phase rules are owned by shared/bench-http-application.js alone (Node http). The trigger client is the runner's curl and generates no load. The cell order is spec §10.4 as is, with a fresh A/B process pair per cell. The runner checks LISTEN sockets on 5220-5239 before the run and after each cell and stops, without moving ports, if one is in use.

Pattern streams.count streams.inFlightPerStream Node implementation
request-serial 1 1 one sequential Promise loop
request-window 1 100 100 Promise workers sharing one socket's logical window
request-backpressure 1 none Promises without a ceiling, yielding to the event loop every 256
send-saturation 8 1 one Promise worker with one gRPC stub or raw ROUTER per stream

4. Values set differently per language

Item Value
warmup 1000 calls (100 in smoke)
gRPC source @grpc/grpc-js unary client stubs, insecure loopback, no tuning
gRPC target @grpc/grpc-js Server with default options, insecure loopback
Node runtime v22.23.2 (measured 2026-09-09)
gRPC packages @grpc/grpc-js 1.14.4, @grpc/proto-loader 0.7.15
ZLink binding published 0.17.6
framework workspace source (the measured commit is recorded in the raw file)

5. Where results go

<OUTROOT>/
├── with_grpc_node_<stamp>.txt
├── unsupported.json            # cells not run, with the reason
└── <implementation>-<pattern>-<payload>-run<run>/
    ├── results.json            # with-grpc-cell-v1: role, trigger, streams, target_stats
    ├── report.txt
    ├── source.log / target.log
    └── target-stats.json
python3 framework/bench/grpc/tools/bench_aggregate.py --lang node --judgement-pattern request-window \
  --runs-glob '<run-1>/*' --runs-glob '<run-2>/*' --runs-glob '<run-3>/*' \
  --runs-glob 'framework/bench/grpc/log/c/<c-run>*' --format full

6. Known limits

  • Every zlink-framework-node cell is unsupported: the framework's protobuf codec does not preserve bytes (a product defect handled separately). The runner starts no process and records the reason in unsupported.json.
  • zlink-node request-window reproduces the completion loss of binding 0.17.6 (decision record FB-049) and is recorded as an error cell; that row is not published until the defect is fixed.
  • The raw comparison is fixed to ROUTER↔ROUTER (no DEALER side run).