---
title: "How to benchmark RPC providers for your workflow"
description: "Benchmark RPC providers on latency under load, block lag, and data consistency, measured separately, on the methods your app calls, from where it runs."
---

# How to benchmark RPC providers for your workflow

<ImageBlock
  src="https://media.alchemy.com/blog/how-to-benchmark-rpc-providers-hero-v3.jpg"
  alt="Benchmark providers on your workload"
  width={1024}
  height={480}
  priority
/>

Picking an RPC provider should be based on benchmarks unique to your workload. We benchmark providers constantly, ourselves included, and we've watched plenty of teams do the same. Sending the requests is the easy part. The hard part is deciding what to measure, then building a test whose result you'd actually stake a decision on.

That's why we wrote the methodology down, and built Alchemy rpcbench to run it: a single binary that benchmarks any set of RPC providers from your own machine, on the methods your app actually calls. The methodology itself works with whatever tooling you've already got.

Three things are worth measuring: latency under load, block lag, and data consistency. Each breaks a different part of your system, and a provider strong on one can be behind on another, so a blended score buries the one that matters to you. Measure what your app actually calls, from where it runs.

## What should you measure when you benchmark an RPC provider?

Latency under load, block lag, and data consistency, measured and reported separately.

<EmbeddedTable
  table={{
    columns: [
      { key: "dimension", width: 160, title: "Dimension", dataType: "object" },
      {
        key: "what",
        width: 260,
        title: "What it measures",
        dataType: "object",
      },
      {
        key: "how",
        width: 360,
        title: "How to measure it",
        dataType: "object",
      },
    ],
    data: [
      {
        dimension: { title: "Latency under load", tooltip: "", icon: "" },
        what: {
          title: "How fast a provider answers requests as volume climbs",
          tooltip: "",
          icon: "",
        },
        how: {
          title:
            "Ramp through a few production RPS tests. Read p95 and p99 at each, with failure and 429 rates beside them",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        dimension: { title: "Block lag", tooltip: "", icon: "" },
        what: {
          title: "How quickly a provider delivers new blocks",
          tooltip: "",
          icon: "",
        },
        how: {
          title:
            "Subscribe to <code>newHeads</code> on every provider at once. Record who announces each block first, and by how much",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
      {
        dimension: { title: "Data consistency", tooltip: "", icon: "" },
        what: {
          title: "Whether a block's data is servable the moment it's announced",
          tooltip: "",
          icon: "",
        },
        how: {
          title:
            "At each new head, ask for the block, its logs, its receipts, and its transactions. Check they agree",
          tooltip: "",
          icon: "",
        },
        id: 2,
      },
    ],
  }}
/>

Data consistency is the one most teams never test. Some providers serve different methods from different backends, and those don't always advance together, so a provider can announce a new head while the logs for that same block come back incomplete. Think of a library that updates its catalog before the books reach the shelves: the catalog says the new arrival is in, and the shelf is empty.

## Which of these should you measure first?

Start with the one that matches what your app does with the data. If you act on new blocks, that's block lag. If you read a block's logs or state the moment it lands, that's data consistency. Otherwise it's latency under load. We've seen teams like Valantis, Aave, RedStone, and Struct each run benchmarks that measure different things.

<EmbeddedTable
  table={{
    columns: [
      {
        key: "building",
        width: 240,
        title: "If you're building",
        dataType: "object",
      },
      { key: "first", width: 260, title: "Measure first", dataType: "object" },
      { key: "risk", width: 280, title: "What's at risk", dataType: "object" },
    ],
    data: [
      {
        building: {
          title:
            'A DEX or onchain quoting engine (e.g. <a href="/case-studies/how-valantis-built-a-reliable-trading-experience-on-hyperliquid">Valantis</a>)',
          tooltip: "",
          icon: "",
        },
        first: {
          title:
            "Block lag, then <strong>p99</strong> on the <code>eth_call</code>s your pricing logic makes",
          tooltip: "",
          icon: "",
        },
        risk: {
          title:
            "You price against a block that has already moved, or your quote lands after the spread closed",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        building: {
          title:
            'A lending protocol with liquidations (e.g. <a href="/case-studies/how-aave-runs-largest-lending-protocol-alchemy">Aave</a>)',
          tooltip: "",
          icon: "",
        },
        first: {
          title:
            "Block lag and data consistency, then <strong>p99</strong> on your health-factor and price reads",
          tooltip: "",
          icon: "",
        },
        risk: {
          title:
            "The liquidation fires a block late, or reads a position before that block's data is servable",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
      {
        building: {
          title:
            'An oracle or event-driven service (e.g. <a href="/case-studies/how-redstone-keeps-price-feeds-reliable-across-51-chains">RedStone</a>)',
          tooltip: "",
          icon: "",
        },
        first: {
          title:
            "Block lag and data consistency, then <strong>p99</strong> on <code>eth_getLogs</code> and <code>eth_call</code>",
          tooltip: "",
          icon: "",
        },
        risk: {
          title: "You miss the trigger because the logs came back incomplete",
          tooltip: "",
          icon: "",
        },
        id: 2,
      },
      {
        building: {
          title:
            'An indexer or wallet backend (e.g. <a href="/case-studies/how-struct-is-building-the-data-layer-for-onchain-finance">Struct</a>)',
          tooltip: "",
          icon: "",
        },
        first: {
          title:
            "<strong>p95</strong> at your peak rate, with failure and 429 rates beside it",
          tooltip: "",
          icon: "",
        },
        risk: {
          title:
            "Backfills stall, or balances go stale while people are trading",
          tooltip: "",
          icon: "",
        },
        id: 3,
      },
    ],
  }}
/>

Then compare providers one method at a time, because a provider that's faster on average can still be slower on the single call your app makes most. That gap is the whole game: a quote arriving after the spread closed is not a slow quote. It's a missed fill.

## How do you run an RPC benchmark you can trust?

Send requests on a fixed schedule to measure latency. The obvious design is to send a request, wait for the answer, then send the next, and that quietly sends fewer requests exactly when a provider starts struggling. Your test eases off at the moment it should be holding steady, so the slowdown you were looking for never shows up in the data. Send on a fixed clock instead, and track how far each send slips behind it. When your own machine falls behind, the numbers describe your machine.

### Setting up the run

<EmbeddedTable
  table={{
    columns: [
      { key: "rule", width: 300, title: "Rule", dataType: "object" },
      { key: "why", width: 480, title: "Why it matters", dataType: "object" },
    ],
    data: [
      {
        rule: {
          title:
            "Test the methods your application actually calls and use the same payload across all providers",
          tooltip: "",
          icon: "",
        },
        why: {
          title:
            "A benchmark only compares what you sent. Run each method alone at full rate, or the expensive ones never gather a stable tail",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        rule: {
          title: "Use an RPS rate that resembles production",
          tooltip: "",
          icon: "",
        },
        why: {
          title:
            "Providers that look identical at 50 RPS can separate at 300. Watch bandwidth too, because a wide <code>eth_getLogs</code> range saturates your link and mimics degradation",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
      {
        rule: { title: "Decide what counts as success", tooltip: "", icon: "" },
        why: {
          title:
            "Latency counts only successful requests, so failing fast earns no credit. Measure time to last byte. Keep 429s in their own bucket",
          tooltip: "",
          icon: "",
        },
        id: 2,
      },
    ],
  }}
/>

### Running it

<EmbeddedTable
  table={{
    columns: [
      { key: "rule", width: 300, title: "Rule", dataType: "object" },
      { key: "why", width: 480, title: "Why it matters", dataType: "object" },
    ],
    data: [
      {
        rule: {
          title: "Warm up every provider identically",
          tooltip: "",
          icon: "",
        },
        why: {
          title:
            "Before each method, for every provider, send ten seconds at full rate and throw it away. Time-based, never a request count: 10 requests at 500 RPS is 20ms of warm-up",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        rule: {
          title: "Hold the schedule, watch your own machine",
          tooltip: "",
          icon: "",
        },
        why: {
          title:
            "Flag the run if send skew p99 passes 5ms over 1,000 samples. At that point you're measuring your machine",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
      {
        rule: {
          title: "Test providers one at a time",
          tooltip: "",
          icon: "",
        },
        why: {
          title:
            "Run them together and they compete for your bandwidth, so whoever wins that looks faster. Give each its own turn, and rotate the order between runs",
          tooltip: "",
          icon: "",
        },
        id: 2,
      },
    ],
  }}
/>

Block lag is the exception. "Who announced this block first" only means something if every provider is watched at the same moment, and it's one small message per block, so nobody competes for your bandwidth.

### Reading the results

<EmbeddedTable
  table={{
    columns: [
      { key: "rule", width: 300, title: "Rule", dataType: "object" },
      { key: "why", width: 480, title: "Why it matters", dataType: "object" },
    ],
    data: [
      {
        rule: {
          title:
            "Measure the gap between the two providers on identical requests. If that gap could plausibly be zero, call it a tie",
          tooltip: "",
          icon: "",
        },
        why: {
          title:
            "Providers inside each other's confidence intervals aren't separated by your data. Below 100 paired samples, don't call it at all",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        rule: {
          title: "Run it twice, from where your system runs",
          tooltip: "",
          icon: "",
        },
        why: {
          title:
            "Latency depends on the network path between you and the provider. Running it twice shows you how performance holds up under different network conditions",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
    ],
  }}
/>

## What is Alchemy rpcbench?

Alchemy rpcbench is a single binary that runs the whole methodology above from your machine, against your own keys. It sends requests only to the endpoints in your config, and the HTML report it produces is sent directly to you.

We built it because writing the methodology down is the easy half. Every rule above takes days to build correctly, and most are invisible when you get them wrong, which is the worst combination in a measurement tool.

The defaults are a starting point. A `workload:` section swaps in your own parameters and methods:

<CodeSnippet
  language="yaml"
  code={`workload:
  - method: eth_call
    params:
      - to: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"  # a contract on your quote path
        data: "0x18160ddd"                                 # and your calldata
      - "$anchor"                                          # the pinned block every provider is asked about`}
/>

Rate steps, duration, timeout, and the block lag window are similarly configurable, and a preflight pass prices the run in requests, time, and bandwidth before any load leaves your machine. That price is in Alchemy compute units, so read it as a size check.

Every run writes a JSON file, CSV aggregates, and a self-contained HTML dashboard. It won't hand you numbers it can't stand behind. Thin samples, a throttled provider, or a machine that fell behind all get flagged at the top.

A run that clears the gate gives you four things, in this order:

<EmbeddedTable
  table={{
    columns: [
      { key: "get", width: 240, title: "What you get", dataType: "object" },
      {
        key: "tells",
        width: 540,
        title: "What it tells you",
        dataType: "object",
      },
    ],
    data: [
      {
        get: { title: "The validity verdict", tooltip: "", icon: "" },
        tells: {
          title:
            "Whether this run is worth quoting at all. Read it before anything else",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        get: { title: "Comparisons", tooltip: "", icon: "" },
        tells: {
          title:
            "Only the gaps wide enough to survive the noise. Overlapping intervals come back as a tie",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
      {
        get: { title: "Per-method latency", tooltip: "", icon: "" },
        tells: {
          title:
            "p50, p90, p95, and p99 at each rate step, with success, failure, and 429 rates beside them",
          tooltip: "",
          icon: "",
        },
        id: 2,
      },
      {
        get: { title: "Block lag and consistency", tooltip: "", icon: "" },
        tells: {
          title:
            "Who announced each block first, how far behind the others landed, missed blocks, and whether each head's data was servable",
          tooltip: "",
          icon: "",
        },
        id: 3,
      },
    ],
  }}
/>

## What this methodology doesn't tell you

- One region says nothing about another. If your system runs elsewhere, run it from there.
- Block lag is relative to the fastest provider you tested, not to the true chain tip.
- Methods run one at a time, so a run is never a replay of your live traffic mix.
- One run is a data point. We strongly recommend multiple runs to get a strong signal on performance, because many factors can affect a reading, including chain-wide issues.
- The Alchemy rpcbench tool is for EVM networks only and supports one network per run.

## How do you get access to Alchemy rpcbench?

Alchemy rpcbench is in private alpha. [Fill out this form](/rpcbench-access) to request access!

And if you'd rather build it yourself with the tooling you already have, the methodology above is all of it. We're happy either way, as long as the number you end up acting on is one you produced.

## Frequently asked questions

### How should I compare RPC providers?

Run the same workload against every provider from the region your system runs in, on the methods it actually calls. Send on a fixed schedule, test providers one at a time, and pin one block so everyone answers the same question. Read the results method by method, with failure and rate-limit counts beside every latency number, then repeat at a different time. If you'd rather not build that yourself, [reach out to us](/rpcbench-access) for access to Alchemy rpcbench.

### What is the difference between RPC latency, block lag, and data consistency?

Latency is how long a provider takes to answer a request you sent. Block lag is how long it takes to tell you a new block exists. Data consistency is whether that block's logs, receipts, and transactions are servable the moment it says so. A provider can be strong on all three, or strong on one and behind on another, which is why they belong in separate columns.

### Can I trust results from a benchmark tool built by a provider?

Yes, and you don't have to take our word for it. No code path in Alchemy rpcbench branches on which provider it's talking to, so provider-specific timeouts or retries have nowhere to hide. Point any coding agent at your clone with the audit prompt from the README and it'll check for you. The tool runs on your machine, against your keys, and the results never leave your output folder.

### Should I choose an RPC provider based on a single overall performance score?

No. A composite score picks the weights for you, and the right weights depend on your application. An indexer backfilling history and an arbitrage bot racing a block need almost nothing in common from a provider. Read the dimension that matches what you're building, then read it method by method.

### Where should I run an RPC benchmark from?

From the region your application runs in. RPC latency depends heavily on the network path between the client and the provider, so a benchmark run from a laptop, or from a cloud region you don't deploy to, measures a path your production traffic never takes.

### How long does an RPC benchmark take to run?

Runtime depends on how you configure the test, but the default configuration takes about two hours. You can configure and start running a test in less than 10 minutes. The config Alchemy rpcbench ships sends 280,000 requests per provider on Ethereum, across seven methods and three rate steps, in roughly 48 minutes, and the block lag window adds another hour. Providers take turns, so the latency phase grows with how many you test. Budget an afternoon, because confidence takes samples.

### Do I need a WebSocket endpoint to benchmark RPC providers?

For latency, no. For block lag and data consistency, yes, because both depend on `eth_subscribe` and subscribing to `newHeads`. You also need at least two providers for block lag, since it measures who announced each block first.

### Which RPC providers can I test?

Any provider with an EVM JSON-RPC endpoint on the chain you're testing. You supply the URLs and the keys, and nothing in the method or the tool depends on who the provider is.

### Does Alchemy publish its own RPC benchmarks?

Yes. Our [benchmarks page](/benchmarks) publishes ongoing provider comparisons you can read today. Alchemy rpcbench is for producing your own, on your application's workload, from the region it runs in.
