RPCBench
Which RPC endpoint is fastest — for this call, from this machine?
A small CLI that compares EVM JSON-RPC over HTTP: latency, P50/P95/P99, error rate, a ranked table, and JSON. Read-only by default. No accounts. No telemetry. Localhost and RFC1918 are allowed (that is how you bench your own node).
It is not a security scanner (Nodeprobe) and not validator monitoring (ValidatorPulse). Split: docs/BOUNDARY.md. How the numbers are computed: docs/METHODOLOGY.md. Roadmap: issues · epic #19.
Install
pip install rpcbench
Python 3.10+. rpcbench --version prints 0.2.0.
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
PRs run pytest, then a live smoke against PublicNode and dRPC (--samples 1 --warmup 0). No local node in CI; the smoke passes if either public endpoint is ok.
Start
rpcbench compare --endpoints https://ethereum.publicnode.com --budget short
rpcbench compare --endpoints endpoints.yaml --profile mix --budget short
Or a YAML/JSON file of named endpoints (keep API keys in a local file; do not commit it):
endpoints:
- name: publicnode
url: https://ethereum.publicnode.com
- name: drpc
url: https://eth.drpc.org
- name: paid
url: https://eth.example/v3/YOUR_KEY
bearer: YOUR_TOKEN
headers:
X-Api-Key: YOUR_KEY
rpcbench run --endpoints endpoints.yaml
rpcbench compare --endpoints endpoints.yaml --json
rpcbench run --endpoints endpoints.yaml -o report.json
run and compare are the same command.
Report
The CLI prints, in order:
- Summary — Fastest (P95 by default; similar-band co-winners, not 81ms vs 84ms)
- Comparison — same numbers in your YAML order (failed rows stay in place; head / lag / fresh / hash / match)
- Ranking — one table, ordered by
--rank-by; similar share a place; high error, stale, or disagree is~; failed last - Methods — per-method P50/P95/P99 and errors when
--profile mix(ranking still uses the whole mix) - Timing — handshake (DNS+TCP+TLS) vs server wait vs payload (body+parse). Not mixed into ranking. Default is keep-alive;
--new-connectionis a cold handshake every request - Tags — one paired
latest/safe/finalizedsnapshot (skipped with a reason if the tag is missing) - Burst — burst vs steady error rate and recovered rps when
--burstis set (same request budget). Extra tag 429s show astags=N, not in timedn/err. - Providers — one table: redacted URL, client, n/err, p95, head/lag/fresh/match, histogram (
≥1s=3), note.--verboseadds per-sample rows - Capabilities — who answered this method
On a TTY, ok is green and fail is red (NO_COLOR or a pipe turns color off). Reports never print API keys, bearer tokens, or header values.
How a run works
Numbers and caveats: docs/METHODOLOGY.md.
Workload
- Paired by default: one shared read-only sequence; each sample is raced to every provider at the same time.
--sequentialis A-then-B. --budgetpicks a named size (short/standard/long). That sets how many samples to take.--max-requestsis the HTTP cap (how many requests the run may send).--samplesand--warmupoverride the named size.longis more samples only — not archive, WebSocket, or tracing unless the workload asks.--profile mixruns a documented read-only mix (head, chainId, getBlockByNumber latest, getBalance of the zero address, eth_call of empty data to the zero address, getLogs latest→latest on the zero address).--samplesis per method. Ranking uses the whole mix, not one cheap head read. Payloads: docs/METHODOLOGY.md.- Burst is opt-in (
--burst N, max 8). The first N timed samples overlap; the rest are a steady phase, optionally capped with--rps. Burst splits the existing sample budget and does not add requests. Burst vs steady error rate and rps are reported separately. Tag 429s aretags=Non that table (not mixed into timed n/err). Default is off (--burst 0,--rps 0). Ramp/spike/soak shapes are a later issue.
Stats
- Warmup is excluded from min/mean/max, jitter, percentiles, error rate, and the histogram.
- P50/P95/P99 are nearest-rank over successful samples. Jitter is the sample standard deviation of those samples (needs n≥2). P99 is the slowest sample until n≥100 (flagged below that).
- Histogram is where successes landed: empty buckets are omitted (
≥1s=3, or<50ms=8 ≥1s=8when split). Buckets:<50ms,<100ms,<250ms,<1s,≥1s(same edges in JSON). - HTTP timing splits each successful sample into handshake (DNS+TCP+TLS), server wait after the connection is ready, and payload (body download + JSON parse). Ranking still uses total RTT. Default reuses keep-alive connections (handshake is ~0 after warmup).
--new-connectionopens a fresh TCP/TLS session every request so distance vs node time is visible. TLS here is handshake latency, not a certificate check. - Error rate is failed/attempted, with a class (timeout, connection, HTTP 4xx/5xx, rate_limit, JSON-RPC, malformed).
rate_limitis HTTP 429 or a CU/throttle JSON-RPC message — reliability, not a scan. - rps in the table is
1000 / mean_msfor this probe — not parallel throughput.--rps Nis a start cap after--burst, not that formula.
Ranking
- Default is P95 of successes (over the mix when
--profile mix). Override with--rank-by p50|p95|p99|mean|rps(throughput=rps). Lower latency wins; higher rps wins. - Similar-band (default 10%) shares a place when the worse value is within that fraction of the better. Error rate above the same band, a stale head, or a disagreeing block hash is not a numbered place (
~). Failed (n_ok=0) never take Fastest.
Extra reads
These are not mixed into latency stats or Fastest.
- Freshness is lag vs the cohort’s upper-median
eth_blockNumberin the same window. Default--stale-blocks 2. Tables print yes when lag is within that tolerance, stale when it exceeds it (Ranking note may add~Ns). Lag time uses--block-timeor a known chain frometh_chainIdalready in the mix (12s on Ethereum). Extra head reads happen only when the workload has noeth_blockNumber. JSON still usesfresh/stale. - Consistency is whether providers return the same block hash at one pinned height (default: that cohort median).
--block HEX|Npins the check when heads naturally diverge by one block. A unique majority hash is canonical; a split is disagreement for everyone who returned a hash. Tables print yes / no under match. Missing/unparseable hashes are unknown, not disagree. JSON still usesagree/disagree. Not fork choice and not a security finding. - Client is a volunteered
web3_clientVersionstring stored as a label (Erigon vs Geth). Missing or hex-only results are omitted. Not a disclosure finding, not outdated-client recon, not a CVE check. - Tags are one paired
eth_getBlockByNumbersnapshot each forlatest,safe, andfinalized. Latency and freshness are per tag vs that tag’s cohort. Unsupported tags are skipped with a reason and do not change Fastest. Full P95 of one tag is--method eth_getBlockByNumber --params '["finalized", false]'.
JSON
--json or -o FILE includes mode, seed, sequence_id, connection (keepalive or new), per-provider id (URL fingerprint, not printed in the CLI table), per-sample pairs (body hashes), jitter_ms, histogram, freshness, consistency, client, tags, burst, HTTP timing percentiles, and burst phases. Reliability score is success rate.
Flags
rpcbench run --endpoints endpoints.yaml --budget short
rpcbench run --endpoints endpoints.yaml --profile mix --budget standard --max-requests 512
rpcbench compare --endpoints http://127.0.0.1:8545
rpcbench run --endpoints endpoints.yaml --rank-by p95
rpcbench run --endpoints endpoints.yaml --burst 4 --rps 2
rpcbench run --endpoints endpoints.yaml --sequential
rpcbench run --endpoints endpoints.yaml --new-connection
rpcbench run --endpoints endpoints.yaml --verbose --json
--budget is a named size (how long to sample). --max-requests is the HTTP cap. --samples / --warmup override the named size.
--budget |
Samples | Warmup | Timeout | Stop after |
|---|---|---|---|---|
short |
3 | 0 | 5s | 30s |
standard (default) |
10 | 1 | 10s | 600s |
long |
50 | 2 | 15s | 1800s |
| Flag | Default | |
|---|---|---|
--budget |
standard |
Named size in the table above |
--samples |
10 | Timed requests per method (overrides --budget) |
--warmup |
1 | Requests excluded from stats (overrides --budget) |
--timeout |
10s | Per-request timeout (overrides --budget) |
--max-requests |
128 | HTTP cap for the whole run (hard cap RPCBENCH_MAX_REQUESTS, default 10000) |
--max-duration |
600s | Stop and still print a report (overrides --budget; 0 = no limit) |
--concurrency |
0 | Paired-wave cap (0 = all providers). Not a load burst |
--burst |
0 | Overlap the first N timed samples (0=off, max 8). Same request budget |
--rps |
0 | Cap starts/sec after --burst (0=off). Does not raise the budget |
--new-connection |
off | Fresh TCP/TLS every request. Default is keep-alive |
--seed |
0 | Shared sequence stamp |
--rank-by |
p95 |
p50, p95, p99, mean, or rps |
--similar-band |
0.10 |
Relative band on the rank key (10%). High error above this is ~, not a place |
--stale-blocks |
2 |
Head lag (blocks vs cohort median) above this is stale. Set per chain |
--block-time |
12 or known chain |
Seconds per block for estimated lag time |
--block |
cohort median | Pin the head-hash check (hex, decimal, or latest) |
--preset |
head (eth_blockNumber), chainId, or balance (eth_getBalance of the zero address) |
|
--profile |
mix — head, chainId, block, balance, call, bounded logs. Do not combine with --method or --preset |
|
--method / --params |
eth_blockNumber |
JSON-RPC method and JSON array of params. Do not combine --method with --preset |
--allow-writes |
off | Required for write methods (eth_send*, personal_*, …) |
--verbose |
off | Print each sample |
--json / -o FILE |
JSON to stdout, and/or write JSON to a file (table still prints unless --json) |
|
--sequential |
off | Run endpoints back-to-back instead of paired |
Safety
Kill switch: RPCBENCH_DISABLED=1, or create ~/.config/rpcbench/DISABLED (override path with RPCBENCH_DISABLE_FILE). RPCBench never prompts for a private key.
License
Release files for rpcbench 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rpcbench-0.2.0.tar.gz | 66.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rpcbench-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 107.3 kB
Release files / rpcbench-0.2.0.tar.gz
| Download URL | rpcbench-0.2.0.tar.gz |
|---|---|
| Size | 66.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a251255e6b6f231b298e78294aa79e71712835f463a58b416728443ea6743d10
|
|
BLAKE2b-256 checksum How to use checksums |
7fbab33f9a63a8384d83983104866efbf65949411e80e66ae83f137596a2b7f9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 25, 2026.
Transparency logRelease files / rpcbench-0.2.0-py3-none-any.whl
| Download URL | rpcbench-0.2.0-py3-none-any.whl |
|---|---|
| Size | 40.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
350dd5de5d18b7f83cb6ea1161bd5a21f6530b4e9e1889907f1da4f8e5e88541
|
|
BLAKE2b-256 checksum How to use checksums |
cabeda6cf83343d90984c63e781f6f061612f2feca919e68a919b1ee96136d53
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 25, 2026.
Transparency log