diff --git a/benchmarks/arbitrum-rpc.yml b/benchmarks/arbitrum-rpc.yml index 8af3fa7d..7e2b4d31 100644 --- a/benchmarks/arbitrum-rpc.yml +++ b/benchmarks/arbitrum-rpc.yml @@ -4,7 +4,7 @@ slug: arbitrum-rpc number: "045" title: Fastest free Arbitrum RPC, live no-key endpoint latency seo_title: "Fastest free Arbitrum RPC 2026" -seo_description: "{{best_name}} leads free Arbitrum RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 8 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Arbitrum RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 7 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Arbitrum RPC endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,7 +14,7 @@ unit: ms higher_is_better: false seo_intro: | - Arbitrum is the second-largest cohort in the cluster: 8 no-key providers including the Arbitrum Foundation's own `arb1.arbitrum.io/rpc`, and one of the few chains where Lava and MeowRPC still compete on a free tier. Foundation endpoints are documented best-effort, and our data shows what that means in practice: a respectable median with a p99 roughly ten times worse. Every provider answers the identical `eth_getBlockByNumber` probe every 60 seconds from us-east, eu-west and Singapore. + Arbitrum is the second-largest cohort in the cluster: 7 no-key providers including the Arbitrum Foundation's own `arb1.arbitrum.io/rpc`, and one of the few chains where Lava and MeowRPC still compete on a free tier. Foundation endpoints are documented best-effort, and our data shows what that means in practice: a respectable median with a p99 roughly ten times worse. Every provider answers the identical `eth_getBlockByNumber` probe every 60 seconds from us-east, eu-west and Singapore. abstract: | Per-chain member of the RPC latency cluster. We measure the round-trip latency of a single, identical RPC call (`eth_getBlockByNumber`) against every no-key public Arbitrum endpoint that sustains continuous probing, 8 providers, every 60 seconds, from us-east, eu-west and Singapore. The harness also classifies every response (ok / http_err / jsonrpc_err / stale / timeout) and audits archive depth every 5 minutes, so the leaderboard rewards sustained, honest availability rather than a fast error message. The cross-chain view lives on the parent rpc-capabilities benchmark; this page is the Arbitrum-scoped answer with per-region breakdowns as a first-class dimension. @@ -26,7 +26,7 @@ methodology: - "Call-result classification: `ok` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "Archive depth: every 5 minutes we issue `eth_getBalance` at (head − depth) for depths from Geth's default pruned cap up to 5M blocks, exposing which free endpoints actually serve historical state." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, methodology and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to chain=\"arbitrum\". Provider coverage: 8 no-key endpoints (PublicNode, dRPC, 1RPC, Tenderly, Nodies, Lava, MeowRPC, Arbitrum). Exclusions follow the cluster-wide rules documented on the parent benchmark." + - "Chain scope: every query on this page is pinned to chain=\"arbitrum\". Provider coverage: 8 no-key endpoints (PublicNode, dRPC, Tenderly, Nodies, Lava, MeowRPC, Arbitrum). Exclusions follow the cluster-wide rules documented on the parent benchmark." findings: - "{{best_name}} currently leads free Arbitrum RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 8 measured providers." @@ -36,9 +36,9 @@ findings: faq: - q: "What is the fastest free Arbitrum RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 8 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 7 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which Arbitrum RPCs work without an API key?" - a: "The 8 providers on this page: PublicNode, dRPC, 1RPC, Tenderly, Nodies, Lava, MeowRPC, Arbitrum. Every (provider, chain) pair was live-verified no-key before inclusion, and anything that key-gates, region-blocks or rate-limits below our 15-second cadence is excluded rather than listed with an asterisk." + a: "The 8 providers on this page: PublicNode, dRPC, Tenderly, Nodies, Lava, MeowRPC, Arbitrum. Every (provider, chain) pair was live-verified no-key before inclusion, and anything that key-gates, region-blocks or rate-limits below our 15-second cadence is excluded rather than listed with an asterisk." - q: "Does the fastest Arbitrum RPC change by region?" a: "Frequently. The headline number averages three probe origins (us-east, eu-west, Singapore), but per-region leaders regularly diverge, a gateway that wins from Virginia can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number on the page to a single origin; pick the one closest to where your requests actually originate." - q: "How is Arbitrum RPC latency measured here?" @@ -111,29 +111,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="arbitrum", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="arbitrum", region="sgp"}[1h]) - - slug: 1rpc - name: 1RPC - tag: Privacy-preserving gateway by Automata Network - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to 1RPC's no-key Arbitrum endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="arbitrum"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="1rpc", chain="arbitrum"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="1rpc", chain="arbitrum"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="1rpc", chain="arbitrum"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="1rpc", chain="arbitrum"}) / sum(ocb:rpc_call:rate_24h{provider="1rpc", chain="arbitrum"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="1rpc", chain="arbitrum"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="arbitrum"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="arbitrum", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="arbitrum", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="arbitrum", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="arbitrum", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="arbitrum", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="arbitrum", region="sgp"}[1h]) - - slug: tenderly name: Tenderly tag: Multi-chain public gateway, 9 chains, no key diff --git a/benchmarks/ethereum-rpc.yml b/benchmarks/ethereum-rpc.yml index 7854f601..ed7d2c33 100644 --- a/benchmarks/ethereum-rpc.yml +++ b/benchmarks/ethereum-rpc.yml @@ -4,7 +4,7 @@ slug: ethereum-rpc number: "044" title: Fastest free Ethereum RPC, live no-key endpoint latency seo_title: "Fastest free Ethereum RPC 2026" -seo_description: "{{best_name}} leads free Ethereum RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 9 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Ethereum RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 8 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Ethereum RPC endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,7 +14,7 @@ unit: ms higher_is_better: false seo_intro: | - Ethereum carries the largest free-RPC cohort we measure: 9 no-key providers answering the same `eth_getBlockByNumber` probe every 60 seconds from three regions. It is also the chain where reliability analysis earns its keep. Cloudflare-eth answers HTTP 200 in well under a second while an increasing share of calls resolve to a JSON-RPC error body (`-32046 Cannot fulfill request`), and Merkle is excluded outright after recurring Cloudflare lockouts that froze our probes for 20 minutes after a single request. If you paste a free RPC URL into an Ethereum dapp, this page is the live answer to which one deserves it. + Ethereum carries the largest free-RPC cohort we measure: 8 no-key providers answering the same `eth_getBlockByNumber` probe every 60 seconds from three regions. It is also the chain where reliability analysis earns its keep. Cloudflare-eth answers HTTP 200 in well under a second while an increasing share of calls resolve to a JSON-RPC error body (`-32046 Cannot fulfill request`), and Merkle is excluded outright after recurring Cloudflare lockouts that froze our probes for 20 minutes after a single request. If you paste a free RPC URL into an Ethereum dapp, this page is the live answer to which one deserves it. abstract: | Per-chain member of the RPC latency cluster. We measure the round-trip latency of a single, identical RPC call (`eth_getBlockByNumber`) against every no-key public Ethereum endpoint that sustains continuous probing, 9 providers, every 60 seconds, from us-east, eu-west and Singapore. The harness also classifies every response (ok / http_err / jsonrpc_err / stale / timeout) and audits archive depth every 5 minutes, so the leaderboard rewards sustained, honest availability rather than a fast error message. The cross-chain view lives on the parent rpc-capabilities benchmark; this page is the Ethereum-scoped answer with per-region breakdowns as a first-class dimension. @@ -26,19 +26,18 @@ methodology: - "Call-result classification: `ok` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`. Latency without reliability is a misleading ranking signal." - "Archive depth: every 5 minutes we issue `eth_getBalance` at (head − depth) for depths from Geth's default pruned cap up to 5M blocks, exposing which free endpoints actually serve historical state." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, methodology and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to chain=\"ethereum\". Provider coverage: 9 no-key endpoints (PublicNode, dRPC, 1RPC, Tenderly, Nodies, Lava, MeowRPC, Flashbots, Cloudflare). Exclusions follow the cluster-wide rules documented on the parent benchmark." + - "Chain scope: every query on this page is pinned to chain=\"ethereum\". Provider coverage: 9 no-key endpoints (PublicNode, dRPC, Tenderly, Nodies, Lava, MeowRPC, Flashbots, Cloudflare). Exclusions follow the cluster-wide rules documented on the parent benchmark." findings: - "{{best_name}} currently leads the free Ethereum RPC field at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 9 measured providers, the largest cohort of any chain in the cluster." - "Cloudflare-eth is the resident cautionary tale: sub-second HTTP 200s that increasingly carry a JSON-RPC error instead of a block number. The success-rate column, not the latency column, tells the real story." - - "The p50-to-p99 spread separates the tiers. {{name:1rpc}} sits at {{p50:1rpc}} median but its p99 regularly runs an order of magnitude higher, while {{name:drpc}} ({{p50:drpc}}) keeps a much tighter distribution." - "Merkle is excluded on Ethereum by design: its endpoint sits behind an aggressive bot filter that locks out programmatic clients for ~20 minutes after one request, invisible on any status page." faq: - q: "What is the fastest free Ethereum RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 9 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 8 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples, so the answer on this page is the answer right now, not a quarterly snapshot. Use the region tabs to see the leader from the origin closest to your deployment." - q: "Which Ethereum RPCs work without an API key?" - a: "The 9 providers on this page: PublicNode, dRPC, 1RPC, Tenderly, Nodies, Lava, MeowRPC, Flashbots, Cloudflare. Every (provider, chain) pair was live-verified no-key before inclusion, and anything that key-gates, region-blocks or rate-limits below our 15-second cadence is excluded rather than listed with an asterisk." + a: "The 9 providers on this page: PublicNode, dRPC, Tenderly, Nodies, Lava, MeowRPC, Flashbots, Cloudflare. Every (provider, chain) pair was live-verified no-key before inclusion, and anything that key-gates, region-blocks or rate-limits below our 15-second cadence is excluded rather than listed with an asterisk." - q: "Does the fastest Ethereum RPC change by region?" a: "Frequently. The headline number averages three probe origins (us-east, eu-west, Singapore), but per-region leaders regularly diverge, a gateway that wins from Virginia can lose from Singapore by multiples. The region tabs at the top of the page re-scope every number on the page to a single origin; pick the one closest to where your requests actually originate." - q: "How is Ethereum RPC latency measured here?" @@ -111,29 +110,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="ethereum", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="ethereum", region="sgp"}[1h]) - - slug: 1rpc - name: 1RPC - tag: Privacy-preserving gateway by Automata Network - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to 1RPC's no-key Ethereum endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="ethereum"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="1rpc", chain="ethereum"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="1rpc", chain="ethereum"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="1rpc", chain="ethereum"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="1rpc", chain="ethereum"}) / sum(ocb:rpc_call:rate_24h{provider="1rpc", chain="ethereum"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="1rpc", chain="ethereum"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="ethereum"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="ethereum", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="ethereum", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="ethereum", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="ethereum", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="1rpc", chain="ethereum", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="1rpc", chain="ethereum", region="sgp"}[1h]) - - slug: tenderly name: Tenderly tag: Multi-chain public gateway, 9 chains, no key diff --git a/benchmarks/rpc-capabilities.yml b/benchmarks/rpc-capabilities.yml index c9ac15ad..b2624366 100644 --- a/benchmarks/rpc-capabilities.yml +++ b/benchmarks/rpc-capabilities.yml @@ -4,7 +4,7 @@ slug: rpc-capabilities number: "010" title: Fastest free public RPC for Ethereum, BNB, Polygon and 7 more EVM chains seo_title: "Fastest free public RPC 2026" -seo_description: "Fastest free public RPC live on eth_getBlockByNumber p50. dRPC, PublicNode, 1RPC, Tenderly, Nodies, Lava ranked." +seo_description: "Fastest free public RPC live on eth_getBlockByNumber p50. dRPC, PublicNode, Tenderly, Nodies, Lava ranked." subtitle: HTTP round-trip latency for eth_getBlockByNumber against free, no-key public RPC endpoints across 10 EVM chains, audited every 60 seconds. category: RPCs status: live @@ -29,7 +29,7 @@ seo_intro: | market; the free public side of the question lives elsewhere, in the RPC services dapps hit when a contributor's free quota is the budget. We probe `eth_getBlockByNumber` every 60 seconds against 15 - audited providers across 10 EVM chains. PublicNode, dRPC, 1RPC, + audited providers across 10 EVM chains. PublicNode, dRPC, MeowRPC, Tenderly Gateway, Nodies (POKT), Lava Network, Merkle, Flashbots Protect, Cloudflare and 5 chain-official foundation endpoints (Base, Binance BSC, Avalanche, Arbitrum, Optimism). @@ -79,7 +79,7 @@ methodology: - "Success gating: because failed probes record no latency, a mostly failing endpoint can post a fast p50 from its rare successes. The ledger pairs every latency figure with a Success column computed as ok calls over total calls in the same 24h window; read rows below 50 percent success as degraded. Providers with under 5 percent success lose their latency series to Prometheus staleness and are pinned below the table as unresponsive instead of being ranked." - "Archive depth: every 5 minutes we issue `eth_getBalance` at (head, depth) for `depth` in {300, 7200, 216_000, 1_296_000, 5_000_000}. Gauge `rpc_archive_depth_supported{depth}` is 1 when the response is non-pruned, 0 otherwise. The 300/7200 thresholds cover Geth's default pruned-cap range; 216k ≈ 1 month; 1.3M ≈ 6 months; 5M ≈ genesis-era full archive." - "Chain coverage: 10 EVM chains. Ethereum (9 providers), Arbitrum (8), Base (6), Optimism (6), Avalanche (6), BNB (5), Polygon (5), Linea (4), Scroll (4), Mantle (4). Provider matrix per chain is documented in `miniapps/rpc-capabilities/cmd/script/config.go`." - - "Excluded by design: Ankr (key-gated), gateway.fm (29 req/IP budget too tight for 15s polling), LlamaRPC + BlockPI + OmniaTech (Cloudflare 521 region-blocked), Alchemy demo (rate-limited dead), NodeReal + GetBlock + Chainstack (key-gated). Merkle on Ethereum + Lava on non-ETH/Arb + MeowRPC on non-ETH/Arb all excluded chain-by-chain. Every (provider, chain) was live-verified no-key before inclusion." + - "Excluded: 1RPC (delisted 2026-07-09, under 13% success even at two chains and one probe per minute), Ankr (key-gated), gateway.fm (29 req/IP budget too tight for 15s polling), LlamaRPC + BlockPI + OmniaTech (Cloudflare 521 region-blocked), Alchemy demo (rate-limited dead), NodeReal + GetBlock + Chainstack (key-gated). Merkle on Ethereum + Lava on non-ETH/Arb + MeowRPC on non-ETH/Arb all excluded chain-by-chain. Every (provider, chain) was live-verified no-key before inclusion." findings: - "Per-chain leaders, not a cross-chain crown. On Ethereum the current leader is {{best_name:chain:ethereum}} at {{best_p50:chain:ethereum}} (p50, 24 h) across 9 measured providers, the largest cohort in this bench. Ethereum is also the chain where Cloudflare-eth's `jsonrpc_err` traps the lazy reader." @@ -95,7 +95,7 @@ faq: - q: "What is the fastest free public RPC right now?" a: "The question is per chain because several providers are single-chain by design (Base official, Binance, Arbitrum Foundation, Optimism Foundation, Avalanche) and a cross-chain aggregate mechanically rewards whichever single-chain endpoint happens to be fastest on its one chain. Current per-chain leaders. Ethereum {{best_name:chain:ethereum}} at {{best_p50:chain:ethereum}}, Base {{best_name:chain:base}} at {{best_p50:chain:base}}, BNB Chain {{best_name:chain:bnb}} at {{best_p50:chain:bnb}}, Arbitrum {{best_name:chain:arbitrum}} at {{best_p50:chain:arbitrum}}. The leaderboard re-sorts every 60 seconds against fresh Prometheus samples averaged across us-east, eu-west and sgp probes. Switch the chain tab to see the leader on the network your product runs on." - q: "Which public RPCs work without an API key in 2026?" - a: "15 audited providers across 10 EVM chains. Universal multi-chain (work on 4+ chains): PublicNode, dRPC, 1RPC, Tenderly Gateway (`gateway.tenderly.co/public/`), Nodies (POKT successor at `*-pokt.nodies.app`). Ethereum-specific or limited footprint: MeowRPC (ETH + Arbitrum only since 2025), Flashbots Protect, Cloudflare (read-degraded), Lava Network (ETH + Arbitrum no-key, other chains require key), Merkle (Base + BSC stable, ETH locked-out by Cloudflare). Chain-official foundation RPCs: Arbitrum (`arb1.arbitrum.io/rpc`), Optimism (`mainnet.optimism.io`), Base (`mainnet.base.org`), Avalanche (`api.avax.network`), BNB (`bsc-dataseed1.binance.org`). Excluded: Ankr (key-gated), gateway.fm (29 req/IP), LlamaRPC / BlockPI / OmniaTech (Cloudflare 521 region-blocked), Alchemy demo (rate-limited dead), NodeReal / GetBlock / Chainstack (key-gated)." + a: "15 audited providers across 10 EVM chains. Universal multi-chain (work on 4+ chains): PublicNode, dRPC, Tenderly Gateway (`gateway.tenderly.co/public/`), Nodies (POKT successor at `*-pokt.nodies.app`). Ethereum-specific or limited footprint: MeowRPC (ETH + Arbitrum only since 2025), Flashbots Protect, Cloudflare (read-degraded), Lava Network (ETH + Arbitrum no-key, other chains require key), Merkle (Base + BSC stable, ETH locked-out by Cloudflare). Chain-official foundation RPCs: Arbitrum (`arb1.arbitrum.io/rpc`), Optimism (`mainnet.optimism.io`), Base (`mainnet.base.org`), Avalanche (`api.avax.network`), BNB (`bsc-dataseed1.binance.org`). Excluded: Ankr (key-gated), gateway.fm (29 req/IP), LlamaRPC / BlockPI / OmniaTech (Cloudflare 521 region-blocked), Alchemy demo (rate-limited dead), NodeReal / GetBlock / Chainstack (key-gated)." - q: "Why is Cloudflare-eth returning errors?" a: "Cloudflare's public Ethereum gateway recently switched to a permissioned mode for many JSON-RPC methods. The endpoint still returns HTTP 200 quickly, but the body is increasingly a JSON-RPC error (`-32046 Cannot fulfill request`) rather than a block number. This benchmark catches the trap by classifying responses as `ok` only when both the HTTP status is 200 and the JSON-RPC body has a usable `result` field. Latency without reliability is a misleading ranking signal for a public RPC." - q: "Why can an RPC with a low success rate still show a fast latency?" diff --git a/benchmarks/rpc-keyed-latency.yml b/benchmarks/rpc-keyed-latency.yml index 59550b9c..8b391906 100644 --- a/benchmarks/rpc-keyed-latency.yml +++ b/benchmarks/rpc-keyed-latency.yml @@ -45,7 +45,7 @@ abstract: | not the provider's paid infrastructure. methodology: - - "Cadence: every 60 seconds per (provider, chain) pair, from each of 3 probe regions (us-east, eu-west, sgp), 4× slower than the no-key bench because keyed free tiers meter monthly quotas. Headline p50/p90/p99 aggregate across regions via Prometheus `avg(quantile_over_time(...))`." + - "Cadence: every 60 seconds per (provider, chain) pair (Infura: every 180 seconds; its free tier's real daily credit budget sits below the documented 3M and returned daily 402 brownouts at 60s, so the cadence is reduced and disclosed, sample sizes stay published), from each of 3 probe regions (us-east, eu-west, sgp), 4× slower than the no-key bench because keyed free tiers meter monthly quotas. Headline p50/p90/p99 aggregate across regions via Prometheus `avg(quantile_over_time(...))`." - "Payload: identical to the no-key bench. EVM: `{\"jsonrpc\":\"2.0\",\"id\":,\"method\":\"eth_getBlockByNumber\",\"params\":[\"latest\",false]}`. Solana: `getSlot` at processed commitment. Non-cacheable by design: the rotating id defeats body-keyed edge caches." - "Authentication: each provider's standard free-tier key, obtained via normal signup, no credit card, no sales contact. Keys live in env vars on the probe services and never in the repo. Endpoint shapes: key-in-path (Alchemy `/v2/`, Infura `/v3/`, Chainstack, Ankr) or query param (Helius `?api-key=`)." - "Quota guard: each region gets 1/3 of a provider's audited monthly free quota; probing pauses at 90% of that budget until calendar-month rollover (`rpc_keyed_quota_used_ratio`, `rpc_call_total{result=\"quota_paused\"}`). Budgets target ≤2/3 of the real quota, so the bench can never exhaust a key." diff --git a/harnesses/rpc-capabilities/cmd/script/config.go b/harnesses/rpc-capabilities/cmd/script/config.go index 94ac6046..6597167b 100644 --- a/harnesses/rpc-capabilities/cmd/script/config.go +++ b/harnesses/rpc-capabilities/cmd/script/config.go @@ -89,7 +89,6 @@ func chains() []Chain { Providers: []Provider{ {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_ETHEREUM_PUBLICNODE", "https://ethereum-rpc.publicnode.com")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_ETHEREUM_DRPC", "https://eth.drpc.org")}, - {Slug: "1rpc", Name: "1RPC", URL: envDefault("RPC_URL_ETHEREUM_1RPC", "https://1rpc.io/eth")}, {Slug: "meowrpc", Name: "MeowRPC", URL: envDefault("RPC_URL_ETHEREUM_MEOWRPC", "https://eth.meowrpc.com")}, {Slug: "flashbots", Name: "Flashbots Protect", URL: envDefault("RPC_URL_ETHEREUM_FLASHBOTS", "https://rpc.flashbots.net")}, {Slug: "cloudflare", Name: "Cloudflare", URL: envDefault("RPC_URL_ETHEREUM_CLOUDFLARE", "https://cloudflare-eth.com")}, @@ -116,7 +115,6 @@ func chains() []Chain { Providers: []Provider{ {Slug: "publicnode", Name: "PublicNode", URL: envDefault("RPC_URL_ARBITRUM_PUBLICNODE", "https://arbitrum-one-rpc.publicnode.com")}, {Slug: "drpc", Name: "dRPC", URL: envDefault("RPC_URL_ARBITRUM_DRPC", "https://arbitrum.drpc.org")}, - {Slug: "1rpc", Name: "1RPC", URL: envDefault("RPC_URL_ARBITRUM_1RPC", "https://1rpc.io/arb")}, {Slug: "meowrpc", Name: "MeowRPC", URL: envDefault("RPC_URL_ARBITRUM_MEOWRPC", "https://arbitrum.meowrpc.com")}, {Slug: "tenderly", Name: "Tenderly Gateway", URL: envDefault("RPC_URL_ARBITRUM_TENDERLY", "https://gateway.tenderly.co/public/arbitrum")}, {Slug: "nodies", Name: "Nodies (POKT)", URL: envDefault("RPC_URL_ARBITRUM_NODIES", "https://arb-pokt.nodies.app")}, diff --git a/harnesses/rpc-keyed-latency/cmd/script/config.go b/harnesses/rpc-keyed-latency/cmd/script/config.go index 446f8478..fb4febc5 100644 --- a/harnesses/rpc-keyed-latency/cmd/script/config.go +++ b/harnesses/rpc-keyed-latency/cmd/script/config.go @@ -24,6 +24,19 @@ type Endpoint struct { // matrix declares which (provider, chain) cells we look for in env. // Kind is derived from the chain. var providers = []string{"infura", "alchemy", "chainstack", "ankr", "helius", "quicknode"} + +// Per-provider probe-interval multiplier. Infura's free tier 402s daily +// at the shared 60s cadence (its real daily credit budget is below the +// documented 3M); 3x (effective 180s) lands at ~0.69M credits/day, +// safely under any plausible cap while keeping 480 samples/day/region. +var intervalMult = map[string]int{"infura": 3} + +func intervalMultFor(provider string) int { + if m, ok := intervalMult[provider]; ok && m > 0 { + return m + } + return 1 +} var chainsEVM = []string{"ethereum", "base", "arbitrum", "optimism", "bnb", "polygon"} func endpoints() []Endpoint { diff --git a/harnesses/rpc-keyed-latency/cmd/script/probe.go b/harnesses/rpc-keyed-latency/cmd/script/probe.go index b3d4554e..d9e944e9 100644 --- a/harnesses/rpc-keyed-latency/cmd/script/probe.go +++ b/harnesses/rpc-keyed-latency/cmd/script/probe.go @@ -211,7 +211,7 @@ func StartProbeLoop(ctx context.Context) { } func probeOne(ctx context.Context, e Endpoint) { - interval := probeInterval() + interval := probeInterval() * time.Duration(intervalMultFor(e.Provider)) jitter := time.Duration(int64(interval) * urlJitter(e.URL+e.Chain) / 100) select { case <-ctx.Done(): diff --git a/infrastructure/monitoring/alertmanager/alertmanager.yml.tmpl b/infrastructure/monitoring/alertmanager/alertmanager.yml.tmpl index 49d6d7ab..fd0f9799 100644 --- a/infrastructure/monitoring/alertmanager/alertmanager.yml.tmpl +++ b/infrastructure/monitoring/alertmanager/alertmanager.yml.tmpl @@ -6,9 +6,25 @@ route: group_wait: 10s group_interval: 30s repeat_interval: 5m - receiver: 'slack-webhook' + # Default: dedicated OCB Slack channel (direct incoming webhook). + receiver: 'slack-direct' + routes: + # Aggregator-latency alerts keep their historical channel via the + # internal relay (#alerting-aggregator-latency). + - matchers: + - app = "aggregator_latency_monitor" + receiver: 'slack-webhook' receivers: + - name: 'slack-direct' + slack_configs: + - api_url: '${SLACK_WEBHOOK_URL}' + send_resolved: true + title: '{{ if eq .Status "firing" }}⚠️{{ else }}✅{{ end }} {{ .CommonLabels.alertname }}' + text: | + {{ range .Alerts }}*{{ .Annotations.summary }}* + {{ .Annotations.description }} + {{ end }} - name: 'slack-webhook' webhook_configs: - url: '${ALERT_WEBHOOK_URL}' diff --git a/infrastructure/monitoring/alertmanager/entrypoint.sh b/infrastructure/monitoring/alertmanager/entrypoint.sh index 2c149d01..2f5afee8 100755 --- a/infrastructure/monitoring/alertmanager/entrypoint.sh +++ b/infrastructure/monitoring/alertmanager/entrypoint.sh @@ -12,7 +12,12 @@ if [ -z "$ALERT_WEBHOOK_URL" ]; then echo "WARNING: ALERT_WEBHOOK_URL not set — alerts will be dropped by the webhook receiver." fi -sed "s|\${ALERT_WEBHOOK_URL}|${ALERT_WEBHOOK_URL}|g" \ +if [ -z "$SLACK_WEBHOOK_URL" ]; then + echo "WARNING: SLACK_WEBHOOK_URL not set — default receiver will drop alerts." +fi + +sed -e "s|\${ALERT_WEBHOOK_URL}|${ALERT_WEBHOOK_URL}|g" \ + -e "s|\${SLACK_WEBHOOK_URL}|${SLACK_WEBHOOK_URL}|g" \ /etc/alertmanager/alertmanager.yml.tmpl \ > /etc/alertmanager/alertmanager.yml diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index c4f48bb1..c82a7755 100644 --- a/src/app/benchmarks/[slug]/page.tsx +++ b/src/app/benchmarks/[slug]/page.tsx @@ -181,9 +181,11 @@ export default async function BenchmarkPage({ (r) => r.value === "all" || regionsWithData.has(r.value), ); const kindOptions = aggregate.dimensions?.kind ?? []; + const venueOptions = aggregate.dimensions?.venue ?? []; const chain = chainOptions[0]?.value ?? null; const region = regionOptions[0]?.value ?? null; const kind = kindOptions[0]?.value ?? null; + const venue = venueOptions[0]?.value ?? null; // Variants (chain × region × kind) are NOT embedded anymore. The old // pre-fetch awaited every variant (rpc-capabilities: 39 full provider @@ -524,9 +526,11 @@ export default async function BenchmarkPage({ chainOptions={chainOptions} regionOptions={regionOptions} kindOptions={kindOptions} + venueOptions={venueOptions} initialChain={chain ?? null} initialRegion={region ?? null} initialKind={kind ?? null} + initialVenue={venue ?? null} hasLongHistory={benchmark.slug === "hyperliquid-frontends"} /> diff --git a/src/components/benchmark-body.tsx b/src/components/benchmark-body.tsx index d317e9e2..c302dfd6 100644 --- a/src/components/benchmark-body.tsx +++ b/src/components/benchmark-body.tsx @@ -123,8 +123,9 @@ function variantKey( chain: string | null, region: string | null, kind: string | null, + venue: string | null = null, ): string { - return `${chain ?? "__none"}|${region ?? "__none"}|${kind ?? "__none"}`; + return `${chain ?? "__none"}|${region ?? "__none"}|${kind ?? "__none"}|${venue ?? "__none"}`; } /** Region values that appear in extras.seriesByRegion24h. Used when the @@ -152,18 +153,22 @@ export function BenchmarkBody({ chainOptions, regionOptions, kindOptions = [], + venueOptions = [], initialChain, initialRegion, initialKind = null, + initialVenue = null, hasLongHistory = false, }: { variants: Record; chainOptions: ChainOption[]; regionOptions: ChainOption[]; kindOptions?: ChainOption[]; + venueOptions?: ChainOption[]; initialChain: string | null; initialRegion: string | null; initialKind?: string | null; + initialVenue?: string | null; /** When true, render the long-window archive toggle (24h..All time) * below the main ledger. Only set on benches whose harness ships a * long-window archive blob (currently: hyperliquid-frontends). */ @@ -178,6 +183,7 @@ export function BenchmarkBody({ const urlChain = searchParams.get("chain"); const urlRegion = searchParams.get("region"); const urlKind = searchParams.get("kind"); + const urlVenue = searchParams.get("venue"); const urlLayer = searchParams.get("layer"); // Canonical-aware lookup: a URL with the new slug ("?chain=gram") // still selects the dimension whose YAML value is the legacy "ton". @@ -189,12 +195,15 @@ export function BenchmarkBody({ (urlRegion && regionOptions.find((r) => r.value === urlRegion)?.value) ?? initialRegion; const resolvedInitialKind = (urlKind && kindOptions.find((k) => k.value === urlKind)?.value) ?? initialKind; + const resolvedInitialVenue = + (urlVenue && venueOptions.find((v) => v.value === urlVenue)?.value) ?? initialVenue; const resolvedInitialLayer: ProviderLayer = urlLayer === "l2" ? "l2" : "l1"; const [chain, setChain] = useState(resolvedInitialChain); const [region, setRegion] = useState(resolvedInitialRegion); const [kind, setKind] = useState(resolvedInitialKind); + const [venue, setVenue] = useState(resolvedInitialVenue); const [layer, setLayer] = useState(resolvedInitialLayer); useEffect(() => { @@ -202,6 +211,7 @@ export function BenchmarkBody({ syncParam(url, "chain", chain, chainOptions); syncParam(url, "region", region, regionOptions); syncParam(url, "kind", kind, kindOptions); + syncParam(url, "venue", venue, venueOptions); // Layer param: drop when default ("l1"), keep when user picked l2. if (layer === "l1") url.searchParams.delete("layer"); else url.searchParams.set("layer", layer); @@ -209,14 +219,16 @@ export function BenchmarkBody({ if (next !== window.location.pathname + window.location.search) { window.history.replaceState(null, "", next); } - }, [chain, region, kind, layer, chainOptions, regionOptions, kindOptions]); + }, [chain, region, kind, venue, layer, chainOptions, regionOptions, kindOptions, venueOptions]); const fallbackChain = chainOptions[0]?.value ?? null; const fallbackRegion = regionOptions[0]?.value ?? null; const fallbackKind = kindOptions[0]?.value ?? null; + const fallbackVenue = venueOptions[0]?.value ?? null; const effectiveChain = chainOptions.length > 0 ? (chain ?? fallbackChain) : null; const effectiveRegion = regionOptions.length > 0 ? (region ?? fallbackRegion) : null; const effectiveKind = kindOptions.length > 0 ? (kind ?? fallbackKind) : null; + const effectiveVenue = venueOptions.length > 0 ? (venue ?? fallbackVenue) : null; // The page ships ONLY the aggregate view (embedding every variant made // ISR regenerations take 30-60 s). Filtered variants are fetched here @@ -225,13 +237,14 @@ export function BenchmarkBody({ // tab still works, numbers stay cross-dimension) and may retry on the // next flip. const [variantMap, setVariantMap] = useState>(variants); - const activeKey = variantKey(effectiveChain, effectiveRegion, effectiveKind); + const activeKey = variantKey(effectiveChain, effectiveRegion, effectiveKind, effectiveVenue); const aggregateBench = - variants[variantKey(null, null, null)] ?? Object.values(variants)[0]; + variants[variantKey(null, null, null, null)] ?? Object.values(variants)[0]; const isAllSelection = (!effectiveChain || effectiveChain === "all") && (!effectiveRegion || effectiveRegion === "all") && - (!effectiveKind || effectiveKind === "all"); + (!effectiveKind || effectiveKind === "all") && + (!effectiveVenue || effectiveVenue === "all"); useEffect(() => { if (variantMap[activeKey] || !aggregateBench) return; // The all/all/all selection IS the aggregate: derived at render time, @@ -242,6 +255,7 @@ export function BenchmarkBody({ if (!isAll(effectiveChain)) qs.set("chain", effectiveChain!); if (!isAll(effectiveRegion)) qs.set("region", effectiveRegion!); if (!isAll(effectiveKind)) qs.set("kind", effectiveKind!); + if (!isAll(effectiveVenue)) qs.set("venue", effectiveVenue!); let cancelled = false; fetch(`/api/bench/${aggregateBench.slug}/variant?${qs.toString()}`) .then((r) => (r.ok ? r.json() : null)) @@ -267,25 +281,24 @@ export function BenchmarkBody({ useEffect(() => { if (!aggregateBench) return; const isAll = (v: string | null) => !v || v === "all"; - const combos: [string | null, string | null, string | null][] = [ - ...chainOptions.map( - (c) => [c.value, effectiveRegion, effectiveKind] as [string | null, string | null, string | null], - ), - ...regionOptions.map( - (r) => [effectiveChain, r.value, effectiveKind] as [string | null, string | null, string | null], - ), + type Combo = [string | null, string | null, string | null, string | null]; + const combos: Combo[] = [ + ...chainOptions.map((c) => [c.value, effectiveRegion, effectiveKind, effectiveVenue] as Combo), + ...regionOptions.map((r) => [effectiveChain, r.value, effectiveKind, effectiveVenue] as Combo), + ...venueOptions.map((v) => [effectiveChain, effectiveRegion, effectiveKind, v.value] as Combo), ]; let cancelled = false; const timers: ReturnType[] = []; let i = 0; - for (const [c, r, k] of combos) { - if (isAll(c) && isAll(r) && isAll(k)) continue; - const key = variantKey(c, r, k); + for (const [c, r, k, vn] of combos) { + if (isAll(c) && isAll(r) && isAll(k) && isAll(vn)) continue; + const key = variantKey(c, r, k, vn); if (variantMap[key]) continue; const qs = new URLSearchParams(); if (!isAll(c)) qs.set("chain", c!); if (!isAll(r)) qs.set("region", r!); if (!isAll(k)) qs.set("kind", k!); + if (!isAll(vn)) qs.set("venue", vn!); timers.push( setTimeout(() => { if (cancelled) return; @@ -528,7 +541,8 @@ export function BenchmarkBody({ {(hasLayerSplit || chainOptions.length > 0 || regionOptions.length > 0 || - kindOptions.length > 0) && ( + kindOptions.length > 0 || + venueOptions.length > 0) && (
{hasLayerSplit && ( setLayer(v as ProviderLayer)} /> )} + {venueOptions.length > 0 && ( + [ + o.value, + summarize( + variantMap[variantKey(effectiveChain, effectiveRegion, effectiveKind, o.value)], + ), + ]) + .filter(([, v]) => v !== null) as [string, ChainMeta][] + )} + /> + )} {kindOptions.length > 0 && ( [ o.value, summarize( - variantMap[variantKey(effectiveChain, effectiveRegion, o.value)], + variantMap[variantKey(effectiveChain, effectiveRegion, o.value, effectiveVenue)], ), ]) .filter(([, v]) => v !== null) as [string, ChainMeta][] @@ -569,7 +601,7 @@ export function BenchmarkBody({ chainOptions .map((o) => [ o.value, - summarize(variantMap[variantKey(o.value, effectiveRegion, effectiveKind)]), + summarize(variantMap[variantKey(o.value, effectiveRegion, effectiveKind, effectiveVenue)]), ]) .filter(([, v]) => v !== null) as [string, ChainMeta][] )} @@ -585,7 +617,7 @@ export function BenchmarkBody({ regionOptions .map((o) => [ o.value, - summarize(variantMap[variantKey(effectiveChain, o.value, effectiveKind)]), + summarize(variantMap[variantKey(effectiveChain, o.value, effectiveKind, effectiveVenue)]), ]) .filter(([, v]) => v !== null) as [string, ChainMeta][] )} diff --git a/src/types/benchmark.ts b/src/types/benchmark.ts index db13e88d..5cff01e3 100644 --- a/src/types/benchmark.ts +++ b/src/types/benchmark.ts @@ -207,6 +207,7 @@ export type Benchmark = { chain?: { value: string; label: string }[]; region?: { value: string; label: string }[]; kind?: { value: string; label: string }[]; + venue?: { value: string; label: string }[]; }; category: "Aggregators" | "Bridges" | "Blockchains" | "Trading" | "Wallets" | "RPCs" | "NFT APIs" | "Explorers"; results: ProviderResult[];