Compare block arrival latency across multiple Robinhood Chain WebSocket feeds, with per-block results, percentile summaries, and Prometheus metrics.
See the coderh-feed-speed is a WebSocket block feed latency comparison tool. It connects to multiple WebSocket block sources at the same time, identifies the same block across sources, and compares their block arrival times.
For every block received by all configured sources, rh-feed-speed reports which source received the block first and how much later the other sources received it. The program also generates a latency percentile summary every 5 minutes and exposes JSON summary and Prometheus metrics endpoints.
rh-feed-speed do?rh-feed-speed can be used to:
A block is included in the completed-block statistics only after it has been received by all configured sources.
-source name=url arguments.blockHash.block:<blockNumber>.-dedup-ttl.At least one -source name=url argument is required. In practice, two or more sources are usually needed to compare block arrival latency.
go run . \
-source official=wss://feed.mainnet.chain.robinhood.com \
-source feeder=wss://us.robinhood-feeder.blockrazor.io/ws/{authToken}
Each source follows this format:
-source name=websocket_url
The source name is used in block details, latency summaries, and Prometheus metric labels.
After startup, the program first prints a header:
[block] sequence_number block_hash fast slow winner
When the same block has been received by all configured sources, the program prints one detail line:
[block] 12345 0xabc... 0s 18ms fast
The fields mean:
| Field | Description |
|---|---|
sequence_number | Sequence number parsed from the WebSocket message. |
block_hash | Block hash used to identify the block. |
| Source column | Source latency relative to the first source that received the block. |
winner | Source that received the block first. |
In this example, fast received the block first. The slow source received the same block 18ms later.
The program automatically outputs a latency summary every 5 minutes:
[summary] type incremental window 5m0s completed_blocks 120
[summary] type incremental source fast winner 80 p10 0s p50 0s p75 2ms p90 5ms p95 8ms p99 20ms p99.9 30ms max 35ms
The summary contains:
| Metric | Description |
|---|---|
window | Time window used for the summary. |
completed_blocks | Number of blocks fully compared in the window. |
winner | Number of blocks first received by the source. |
p10 | 10th-percentile latency. |
p50 | Median latency. |
p75 | 75th-percentile latency. |
p90 | 90th-percentile latency. |
p95 | 95th-percentile latency. |
p99 | 99th-percentile latency. |
p99.9 | 99.9th-percentile latency. |
max | Maximum recorded latency. |
Each percentile value represents that source’s latency relative to the source that received the corresponding block first.
Request the current summary in JSON format:
curl http://127.0.0.1:9092/summary
The /summary endpoint currently returns a 5-minute window summary using:
SummaryWithPercentiles(now, 5*time.Minute)
Prometheus metrics are exposed at:
curl http://127.0.0.1:9092/metrics
The currently available metric is:
speed_test_source_wins_total{source="xxx"} 80
This counter records how many fully compared blocks were first received by the specified source.
Both the automatic 5-minute summary and the /summary endpoint call:
SummaryWithPercentiles(now, 5*time.Minute)
The implementation generates the summary as follows:
This avoids holding the tracker lock during the complete summary calculation.
The window fields are calculated as follows:
completed_blocks is the number of fully compared blocks in the window.winner_counts[].count is the number of wins for each source.winner_counts[].percentiles contains the latency percentiles for each source.-dedup-ttlWindow statistics are based only on records still retained in the tracker. The -dedup-ttl setting therefore directly affects how much data the summary can inspect.
To inspect a complete 5-minute window, set -dedup-ttl to at least 5m. For example:
go run . \
-dedup-ttl 10m \
-source official=wss://feed.mainnet.chain.robinhood.com \
-source feeder=wss://us.robinhood-feeder.blockrazor.io/ws/{authToken}
With the default 30s TTL, the 5-minute summary can only count records from roughly the most recent 30 seconds that have not already been cleaned up.
Recommended relationship:
summary window: 5m
dedup TTL: >= 5m
The tracker supports two summary scopes:
| Method | Summary scope |
|---|---|
SummaryWithPercentiles(now, 5*time.Minute) | Uses retained records from the 5-minute window. |
Summary(now, 0) | Uses accumulated WinCount and DelaySamples since the process started. |
The automatic logs and HTTP /summary endpoint currently use the 5-minute window summary.
Incoming messages are currently parsed according to this JSON structure:
{
"version": 1,
"messages": [
{
"sequenceNumber": 12345,
"blockHash": "0xabc...",
"message": {
"message": {
"header": {
"blockNumber": 100
}
}
}
}
]
}
The parser reads these fields:
| JSON field | Usage |
|---|---|
messages[].sequenceNumber | Sequence number displayed in block output. |
messages[].blockHash | Primary ID used to match the same block across sources. |
messages[].message.message.header.blockNumber | Fallback block number when no hash is available. |
If a WebSocket message does not contain a recognizable block, it is skipped. When -debug is enabled, parsing errors are printed.
Tracker.RecordBlock identifies blocks using the following order:
blockHash as the block ID.block:<blockNumber>.The first source to record a block becomes the winner. Delays for later sources are calculated as:
now - FirstSeenAt
The block is marked as completed after all configured sources have received it.
The main program flow is implemented in main.go:
-source-subscribe message after the WebSocket connects.messages[].blockHash, sequenceNumber, and blockNumber.Tracker.RecordBlock.block:<blockNumber> as the fallback.FirstSeenAt.rh-feed-speed?rh-feed-speed is a program that compares the arrival time of the same block across multiple WebSocket block sources.
At least one source is required to start the program. Two or more sources are usually needed to compare latency.
The first configured source to receive a block is recorded as the winner. Other source delays are calculated relative to that first arrival.
A block is counted as completed after it has been received by all configured sources.
The summary can only use records still retained by the tracker. If -dedup-ttl is shorter than five minutes, older records may be removed before the summary is calculated.
-dedup-ttl?Use at least -dedup-ttl 5m when the complete five-minute summary window is required. The original example uses -dedup-ttl 10m.
If a block number is available, the tracker uses block:<blockNumber> as the block ID. Otherwise, the message is skipped.
The current metric is speed_test_source_wins_total, which counts wins for each source.
Enable -debug to print message parsing errors.
2 commits
Go
100.0%
Compare block arrival latency across multiple Robinhood Chain WebSocket feeds, with per-block results, percentile summaries, and Prometheus metrics.
See the coderh-feed-speed is a WebSocket block feed latency comparison tool. It connects to multiple WebSocket block sources at the same time, identifies the same block across sources, and compares their block arrival times.
For every block received by all configured sources, rh-feed-speed reports which source received the block first and how much later the other sources received it. The program also generates a latency percentile summary every 5 minutes and exposes JSON summary and Prometheus metrics endpoints.
rh-feed-speed do?rh-feed-speed can be used to:
A block is included in the completed-block statistics only after it has been received by all configured sources.
-source name=url arguments.blockHash.block:<blockNumber>.-dedup-ttl.At least one -source name=url argument is required. In practice, two or more sources are usually needed to compare block arrival latency.
go run . \
-source official=wss://feed.mainnet.chain.robinhood.com \
-source feeder=wss://us.robinhood-feeder.blockrazor.io/ws/{authToken}
Each source follows this format:
-source name=websocket_url
The source name is used in block details, latency summaries, and Prometheus metric labels.
After startup, the program first prints a header:
[block] sequence_number block_hash fast slow winner
When the same block has been received by all configured sources, the program prints one detail line:
[block] 12345 0xabc... 0s 18ms fast
The fields mean:
| Field | Description |
|---|---|
sequence_number | Sequence number parsed from the WebSocket message. |
block_hash | Block hash used to identify the block. |
| Source column | Source latency relative to the first source that received the block. |
winner | Source that received the block first. |
In this example, fast received the block first. The slow source received the same block 18ms later.
The program automatically outputs a latency summary every 5 minutes:
[summary] type incremental window 5m0s completed_blocks 120
[summary] type incremental source fast winner 80 p10 0s p50 0s p75 2ms p90 5ms p95 8ms p99 20ms p99.9 30ms max 35ms
The summary contains:
| Metric | Description |
|---|---|
window | Time window used for the summary. |
completed_blocks | Number of blocks fully compared in the window. |
winner | Number of blocks first received by the source. |
p10 | 10th-percentile latency. |
p50 | Median latency. |
p75 | 75th-percentile latency. |
p90 | 90th-percentile latency. |
p95 | 95th-percentile latency. |
p99 | 99th-percentile latency. |
p99.9 | 99.9th-percentile latency. |
max | Maximum recorded latency. |
Each percentile value represents that source’s latency relative to the source that received the corresponding block first.
Request the current summary in JSON format:
curl http://127.0.0.1:9092/summary
The /summary endpoint currently returns a 5-minute window summary using:
SummaryWithPercentiles(now, 5*time.Minute)
Prometheus metrics are exposed at:
curl http://127.0.0.1:9092/metrics
The currently available metric is:
speed_test_source_wins_total{source="xxx"} 80
This counter records how many fully compared blocks were first received by the specified source.
Both the automatic 5-minute summary and the /summary endpoint call:
SummaryWithPercentiles(now, 5*time.Minute)
The implementation generates the summary as follows:
This avoids holding the tracker lock during the complete summary calculation.
The window fields are calculated as follows:
completed_blocks is the number of fully compared blocks in the window.winner_counts[].count is the number of wins for each source.winner_counts[].percentiles contains the latency percentiles for each source.-dedup-ttlWindow statistics are based only on records still retained in the tracker. The -dedup-ttl setting therefore directly affects how much data the summary can inspect.
To inspect a complete 5-minute window, set -dedup-ttl to at least 5m. For example:
go run . \
-dedup-ttl 10m \
-source official=wss://feed.mainnet.chain.robinhood.com \
-source feeder=wss://us.robinhood-feeder.blockrazor.io/ws/{authToken}
With the default 30s TTL, the 5-minute summary can only count records from roughly the most recent 30 seconds that have not already been cleaned up.
Recommended relationship:
summary window: 5m
dedup TTL: >= 5m
The tracker supports two summary scopes:
| Method | Summary scope |
|---|---|
SummaryWithPercentiles(now, 5*time.Minute) | Uses retained records from the 5-minute window. |
Summary(now, 0) | Uses accumulated WinCount and DelaySamples since the process started. |
The automatic logs and HTTP /summary endpoint currently use the 5-minute window summary.
Incoming messages are currently parsed according to this JSON structure:
{
"version": 1,
"messages": [
{
"sequenceNumber": 12345,
"blockHash": "0xabc...",
"message": {
"message": {
"header": {
"blockNumber": 100
}
}
}
}
]
}
The parser reads these fields:
| JSON field | Usage |
|---|---|
messages[].sequenceNumber | Sequence number displayed in block output. |
messages[].blockHash | Primary ID used to match the same block across sources. |
messages[].message.message.header.blockNumber | Fallback block number when no hash is available. |
If a WebSocket message does not contain a recognizable block, it is skipped. When -debug is enabled, parsing errors are printed.
Tracker.RecordBlock identifies blocks using the following order:
blockHash as the block ID.block:<blockNumber>.The first source to record a block becomes the winner. Delays for later sources are calculated as:
now - FirstSeenAt
The block is marked as completed after all configured sources have received it.
The main program flow is implemented in main.go:
-source-subscribe message after the WebSocket connects.messages[].blockHash, sequenceNumber, and blockNumber.Tracker.RecordBlock.block:<blockNumber> as the fallback.FirstSeenAt.rh-feed-speed?rh-feed-speed is a program that compares the arrival time of the same block across multiple WebSocket block sources.
At least one source is required to start the program. Two or more sources are usually needed to compare latency.
The first configured source to receive a block is recorded as the winner. Other source delays are calculated relative to that first arrival.
A block is counted as completed after it has been received by all configured sources.
The summary can only use records still retained by the tracker. If -dedup-ttl is shorter than five minutes, older records may be removed before the summary is calculated.
-dedup-ttl?Use at least -dedup-ttl 5m when the complete five-minute summary window is required. The original example uses -dedup-ttl 10m.
If a block number is available, the tracker uses block:<blockNumber> as the block ID. Otherwise, the message is skipped.
The current metric is speed_test_source_wins_total, which counts wins for each source.
Enable -debug to print message parsing errors.
2 commits
Go
100.0%