Validate and benchmark EVM JSON-RPC endpoints with 16 probes across HTTP and WebSocket. Measures latency, batch limits, rate limiting, throughput, WebSocket stability, and more — all from a single module.
git clone https://github.com/a6b8/rpc-benchmark.git
cd rpc-benchmark
npm iimport { RpcBenchmark } from 'rpc-benchmark'
const benchmark = new RpcBenchmark()
const { operational } = await benchmark.runOperational( {
'urls': [
{ 'url': 'https://ethereum-rpc.publicnode.com', 'protocol': 'http' },
{ 'url': 'wss://ethereum-rpc.publicnode.com', 'protocol': 'ws' }
]
} )- 16 Probes — 6 operational (health validation) + 10 research (performance analysis)
- Dual Protocol — HTTP and WebSocket support with protocol-specific probes
- Event-Driven — Progress, complete, and error events via EventEmitter
- Parallel Execution — Configurable chunking for concurrent URL processing
- Provider Detection — Fingerprints Alchemy, Infura, QuickNode, Ankr, PublicNode, Chainstack, dRPC
- 26 Error Codes — Structured codes across 5 categories with descriptions and recovery guidance
- Zero Config — Sensible defaults, works out of the box against any EVM endpoint
All methods follow the same return pattern: probe results include status, data, and metrics with latencyMs, samples, and errors.
Creates a new RpcBenchmark instance.
Method
new RpcBenchmark( { silent } )
| Key | Type | Description | Required |
|---|---|---|---|
| silent | boolean | Suppress console output. Default false |
No |
Example
const benchmark = new RpcBenchmark( { 'silent': true } )Runs 6 operational probes against all provided URLs. Tests basic health: chain ID, block sync, gas price, network version, archive support, client version.
Method
.runOperational( { urls, options, chunkSize } )
| Key | Type | Description | Required |
|---|---|---|---|
| urls | array of objects | Endpoints to test. Each: { url: string, protocol: 'http' | 'ws' } |
Yes |
| options | object | Probe-specific options keyed by probe name | No |
| chunkSize | number | URLs processed in parallel. Default 10 |
No |
Example
const { operational } = await benchmark.runOperational( {
'urls': [
{ 'url': 'https://ethereum-rpc.publicnode.com', 'protocol': 'http' },
{ 'url': 'wss://ethereum-rpc.publicnode.com', 'protocol': 'ws' }
],
'options': {
'chainId': { 'expectedChainId': 1 },
'netVersion': { 'expectedNetVersion': '1' }
}
} )Returns
{
operational: [
{
url: 'https://...',
protocol: 'http',
status: true,
probes: {
chainId: { status, data, metrics },
blockSync: { status, data, metrics },
gasPrice: { status, data, metrics },
netVersion: { status, data, metrics },
archive: { status, data, metrics },
clientVersion: { status, data, metrics }
}
}
]
}Runs all 16 probes (6 operational + 10 research). Includes latency statistics, batch limits, rate limiting detection, throughput measurement, and WebSocket stability.
Method
.runResearch( { urls, options, chunkSize } )
| Key | Type | Description | Required |
|---|---|---|---|
| urls | array of objects | Endpoints to test. Each: { url: string, protocol: 'http' | 'ws' } |
Yes |
| options | object | Probe-specific options keyed by probe name | No |
| chunkSize | number | URLs processed in parallel. Default 5 |
No |
Example
const { research, summary } = await benchmark.runResearch( {
'urls': [ { 'url': 'https://ethereum-rpc.publicnode.com', 'protocol': 'http' } ],
'options': {
'latency': { 'sampleCount': 50 },
'batchLimit': { 'maxSearch': 500 },
'throughput': { 'durationMs': 5000 }
}
} )Returns
{
research: [
{
url: 'https://...',
protocol: 'http',
status: true,
operational: { chainId: {...}, blockSync: {...}, ... },
research: { latency: {...}, batchLimit: {...}, ... }
}
],
summary: { total: 1, passed: 1, failed: 0 }
}Runs a single named probe against all provided URLs.
Method
.runProbe( { probeName, urls, options } )
| Key | Type | Description | Required |
|---|---|---|---|
| probeName | string | Name of the probe to run (see Probes) | Yes |
| urls | array of objects | Endpoints to test | Yes |
| options | object | Options for the specific probe | No |
Example
const { results, summary } = await benchmark.runProbe( {
'probeName': 'latency',
'urls': [ { 'url': 'https://ethereum-rpc.publicnode.com', 'protocol': 'http' } ],
'options': { 'sampleCount': 10 }
} )Returns
{
results: [ { url, protocol, status, probes: { latency: { status, data, metrics } } } ],
summary: { total: 1, passed: 1, failed: 0 }
}Returns metadata for all available probes. Static method — no instance required.
Method
RpcBenchmark.getProbeList()
Example
const { probeList } = RpcBenchmark.getProbeList()Returns
{
probeList: [
{ name: 'chainId', category: 'operational', protocols: ['http','ws'], description: '...' },
{ name: 'latency', category: 'research', protocols: ['http','ws'], description: '...' },
...
]
}Health validation probes that verify an endpoint is functional and correctly configured.
| Probe | Protocols | Description |
|---|---|---|
chainId |
http, ws | Verifies chain identity via eth_chainId. Options: expectedChainId |
blockSync |
http, ws | Checks block sync status via eth_blockNumber. Options: maxDrift, referenceBlock |
gasPrice |
http, ws | Validates data freshness via eth_gasPrice |
netVersion |
http, ws | Confirms network identity via net_version. Options: expectedNetVersion |
archive |
http, ws | Tests archive node capability via eth_getLogs on early blocks. Options: fromBlock, toBlock |
clientVersion |
http, ws | Retrieves node software version via web3_clientVersion |
Performance analysis probes that measure endpoint capabilities and limits.
| Probe | Protocols | Description |
|---|---|---|
latency |
http, ws | Multi-sample latency measurement with min/max/mean/median/p95/p99/stddev. Options: sampleCount (default: 20) |
batchLimit |
http | Determines max JSON-RPC batch size via binary search. Options: maxSearch (default: 1000) |
multicallStress |
http | Finds max Multicall3 aggregate payload size. Options: maxCalls (default: 500), step (default: 10) |
rateLimit |
http | Detects rate limiting via parallel burst requests. Options: burstSize (default: 50) |
throughput |
http | Measures sustained requests per second. Options: durationMs (default: 10000) |
responseSizeLimit |
http | Determines max eth_getLogs response size by expanding block ranges. Options: startRange, maxRange |
providerFingerprint |
http | Identifies provider (Alchemy, Infura, QuickNode, Ankr, PublicNode, Chainstack, dRPC) via headers and error patterns |
wsStability |
ws | Tests long-running WebSocket connection stability with periodic pings. Options: durationMs, pingIntervalMs |
wsSubscription |
ws | Tests newHeads subscription and block delivery latency. Options: durationMs |
wsConcurrency |
ws | Determines max parallel WebSocket connections. Options: maxConnections (default: 20) |
All errors follow the structure { code: string, message: string }. The code field uses the format CATEGORY_NAME and can be looked up via the ErrorLookup helper.
import { ErrorLookup } from 'rpc-benchmark'
// Look up a specific error
const { error } = ErrorLookup.getByCode( { 'code': 'PROBE_TIMEOUT' } )
// → { code, category, severity, recoverable, message, description }
// Get all errors for a category
const { errors } = ErrorLookup.getByCategory( { 'category': 'net' } )
// Get all recoverable errors
const { recoverable } = ErrorLookup.getRecoverable()
// Get all 26 error codes
const { codes } = ErrorLookup.getAllCodes()Validation errors — thrown before any probe executes.
| Code | Severity | Message |
|---|---|---|
INPUT_MISSING_URLS |
error | The urls parameter is missing or undefined |
INPUT_INVALID_URL_FORMAT |
error | A url entry has an invalid format |
INPUT_INVALID_PROTOCOL |
error | Protocol value is not supported |
INPUT_INVALID_OPTIONS |
error | Options parameter has an invalid format |
INPUT_INVALID_CHUNK_SIZE |
error | Chunk size value is invalid |
INPUT_UNKNOWN_PROBE |
error | Probe name is not recognized |
Probe-level errors — returned in metrics.errors of individual probe results.
| Code | Severity | Recoverable | Message |
|---|---|---|---|
PROBE_TIMEOUT |
warn | Yes | Probe execution timed out |
PROBE_UNEXPECTED_RESULT |
warn | Yes | Probe received unexpected or unparseable data |
PROBE_CHAIN_ID_MISMATCH |
error | No | Chain ID does not match the expected value |
PROBE_BLOCK_DRIFT |
warn | Yes | Block number drift exceeds the allowed threshold |
PROBE_NOT_ARCHIVE |
info | Yes | Endpoint does not support archive queries |
PROBE_EXCEPTION |
error | Yes | Probe threw an unexpected exception |
Network-level errors — connection, DNS, TLS, and HTTP failures.
| Code | Severity | Recoverable | Message |
|---|---|---|---|
NET_CONNECTION_REFUSED |
error | Yes | Connection to the endpoint was refused |
NET_DNS_RESOLUTION_FAILED |
error | No | DNS lookup for the endpoint hostname failed |
NET_TLS_ERROR |
error | No | TLS/SSL handshake failed |
NET_FETCH_FAILED |
error | Yes | HTTP request failed at the network level |
NET_HTTP_ERROR |
error | Yes | HTTP response returned a non-2xx status code |
JSON-RPC protocol errors — invalid responses, unsupported methods, rate limits.
| Code | Severity | Recoverable | Message |
|---|---|---|---|
RPC_INVALID_RESPONSE |
error | Yes | RPC response has an invalid or unexpected format |
RPC_METHOD_NOT_FOUND |
warn | No | The requested RPC method is not supported |
RPC_RATE_LIMITED |
warn | Yes | Request was rate limited by the provider |
RPC_SERVER_ERROR |
error | Yes | RPC server returned an internal error |
WebSocket-specific errors — connection, messaging, subscriptions.
| Code | Severity | Recoverable | Message |
|---|---|---|---|
WS_CONNECTION_FAILED |
error | Yes | WebSocket connection could not be established |
WS_MESSAGE_TIMEOUT |
warn | Yes | WebSocket message response timed out |
WS_UNEXPECTED_CLOSE |
warn | Yes | WebSocket connection was closed unexpectedly |
WS_SUBSCRIPTION_FAILED |
error | Yes | WebSocket subscription was rejected by the provider |
WS_CLOSE_FAILED |
info | Yes | WebSocket close operation failed |
RpcBenchmark extends EventEmitter and emits three event types:
const benchmark = new RpcBenchmark()
benchmark.on( 'progress', ( { completed, total, percent, url, protocol } ) => {
console.log( `${percent}% (${completed}/${total})` )
} )
benchmark.on( 'complete', ( { total, results } ) => {
console.log( `Done: ${total} URLs tested` )
} )
benchmark.on( 'error', ( { url, protocol, probe, error } ) => {
console.log( `Error in ${probe}: ${error}` )
} )| Event | Payload | Description |
|---|---|---|
progress |
{ completed, total, percent, url, protocol } |
Emitted after each URL completes |
complete |
{ total, results } |
Emitted when all URLs are done |
error |
{ url, protocol, probe, error } |
Emitted on unhandled probe exception |
Contributions are welcome. Please open an issue or submit a pull request.
npm test # Run all tests
npm run test:coverage:src # Run with coverage