How to Check Optimism RPC Reliability Before You Ship

A practical guide to measuring Optimism RPC reliability yourself with a small Node.js health-check script, plus how to separate RPC access from indexing and querying.

"Which Optimism RPC is most reliable?" is the wrong question to ask a blog post, because reliability depends on your workload, your region, and the moment you measure it. The right move is to measure it yourself against the endpoints you are actually considering. This guide gives you a concrete task: build a small Node.js 18+ script that probes an Optimism RPC endpoint for latency, error rate, and chain identity, then decide whether it is good enough for your app. You will also see where RPC access stops and indexing begins, so you do not blame your provider for a query problem it was never meant to solve.

The task: probe an endpoint, not a marketing page

Your concrete task is to write a script that sends a fixed set of JSON-RPC requests to an Optimism endpoint, records how long each takes, counts failures, and confirms the endpoint is actually on the Optimism chain. Run it repeatedly over a window you care about (for example, every 30 seconds for an hour) and compare the numbers across the endpoints you are evaluating.

Do not trust a single sample. A single fast response tells you almost nothing. What you want is a distribution: median latency, tail latency, and the fraction of calls that fail. Those are things only you can measure for your own traffic pattern and region.

If you need a starting point for an Optimism endpoint, the OnFinality Optimism network page lists access details. Use whatever endpoint you have and plug it into the script below.

  • Measure latency, error rate, and chain identity, not vibes
  • Run the probe over a real time window, not once
  • Compare endpoints under the same conditions
JavaScript · Node.js
// probe-optimism.mjs — Node.js 18+ (global fetch)
// Usage: OPTIMISM_RPC_URL="https://your-endpoint" node probe-optimism.mjs

const url = process.env.OPTIMISM_RPC_URL;
if (!url) {
  console.error("Set OPTIMISM_RPC_URL to an Optimism HTTP RPC endpoint.");
  process.exit(1);
}

// Optimism mainnet chain ID is 10. Confirm before trusting any endpoint.
const EXPECTED_CHAIN_ID = 10;

async function rpc(method, params = []) {
  const started = performance.now();
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 5000);
  try {
    const res = await fetch(url, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
      signal: controller.signal,
    });
    const ms = performance.now() - started;
    if (!res.ok) {
      return { ok: false, ms, error: `HTTP ${res.status}` };
    }
    const body = await res.json();
    if (body.error) {
      return { ok: false, ms, error: JSON.stringify(body.error) };
    }
    return { ok: true, ms, result: body.result };
  } catch (err) {
    const ms = performance.now() - started;
    const reason = err.name === "AbortError" ? "timeout" : err.message;
    return { ok: false, ms, error: reason };
  } finally {
    clearTimeout(timer);
  }
}

async function main() {
  const chain = await rpc("eth_chainId");
  if (!chain.ok) {
    console.error("Chain ID check failed:", chain.error);
    process.exit(2);
  }
  const chainId = parseInt(chain.result, 16);
  if (chainId !== EXPECTED_CHAIN_ID) {
    console.error(`Wrong chain: got ${chainId}, expected ${EXPECTED_CHAIN_ID}. Aborting.`);
    process.exit(3);
  }

  const samples = [];
  for (let i = 0; i < 10; i++) {
    const r = await rpc("eth_blockNumber");
    samples.push(r);
    await new Promise((res) => setTimeout(res, 500));
  }

  const ok = samples.filter((s) => s.ok);
  const latencies = ok.map((s) => s.ms).sort((a, b) => a - b);
  const median = latencies.length
    ? latencies[Math.floor(latencies.length / 2)].toFixed(0)
    : "n/a";
  const p90 = latencies.length
    ? latencies[Math.floor(latencies.length * 0.9)].toFixed(0)
    : "n/a";

  console.log(JSON.stringify({
    chainId,
    samples: samples.length,
    ok: ok.length,
    failed: samples.length - ok.length,
    medianMs: median,
    p90Ms: p90,
    errors: samples.filter((s) => !s.ok).map((s) => s.error),
  }, null, 2));
}

main();

Expected result: a JSON object with chainId: 10, a count of successful and failed samples, a median and p90 latency in milliseconds, and any error strings. If the chain ID is not 10, the script exits early, because a fast endpoint on the wrong chain is worse than a slow one on the right chain.

The failure handling matters. A non-200 HTTP status, a JSON-RPC error object, a network exception, and a timeout are all treated as failures and recorded rather than thrown. That way one bad sample does not kill your measurement run. The 5-second abort keeps a hung connection from stalling the loop.

Reading the numbers without overclaiming

Ten samples is a smoke test, not a verdict. To make a real decision, wrap the loop in a longer run and log each sample with a timestamp, then compute error rate and tail latency over the whole window. If you want a second opinion on provider selection criteria, the OnFinality RPC Assistant is a reasonable place to compare options, but treat any published comparison as a starting point and confirm with your own probe.

Be careful about what your numbers mean. Latency from your laptop is not latency from your production region. A quiet test at 3 a.m. is not a busy test at peak. And a single endpoint that looks great today can change tomorrow, which is why you should keep the probe running in CI or as a cron job rather than running it once.

Do not invent benchmarks for a provider. If you need a performance claim, measure it. If you cannot measure it, say so.

  • Report error rate and p90, not just the fastest sample
  • Run from the region and network path your app actually uses
  • Keep the probe running so you notice regressions
bash
# Quick one-off check with curl (replace the URL with your endpoint)
curl -s -X POST "$OPTIMISM_RPC_URL" \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'

# Expected: {"jsonrpc":"2.0","id":1,"result":"0xa"}
# 0xa is hex for 10, the Optimism mainnet chain ID.

If curl returns an HTML error page or a 401/403, your endpoint URL or auth header is wrong. If it returns a JSON-RPC error, read the message before assuming the provider is down. Many "outages" are misconfigured URLs, missing API keys, or hitting a testnet endpoint with mainnet expectations.

RPC access is not indexing or querying

A common source of confusion: developers blame their RPC endpoint for slow or impossible queries that RPC was never designed to answer. JSON-RPC gives you point lookups and block-level data: eth_blockNumber, eth_getBalance, eth_getLogs within a block range, and so on. It does not give you a fast, arbitrary, historical query across millions of events.

If you need to answer questions like "how many wallets interacted with this contract last month" or "list all transfers for this address with pagination," that is an indexing problem. You either run an indexer or use a hosted indexing service. RPC reliability and indexing reliability are different concerns with different failure modes.

This distinction is worth internalizing before you switch providers hoping to fix a query problem. Switching RPC endpoints will not make an unbounded eth_getLogs call fast; it will just fail differently. For background on how indexing fits into the stack, the OnFinality Learn section covers the concepts.

  • RPC: point lookups, block data, bounded log queries
  • Indexing: arbitrary historical queries, aggregations, pagination
  • Diagnose which layer is failing before changing providers
JavaScript · Node.js
// Bounded eth_getLogs is an RPC call. Unbounded historical scans are not.
// This asks for logs in a small, explicit block range.
const fromBlock = "0x" + (150000000).toString(16);
const toBlock = "0x" + (150000100).toString(16);

const res = await fetch(process.env.OPTIMISM_RPC_URL, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "eth_getLogs",
    params: [{ fromBlock, toBlock }],
  }),
});

const body = await res.json();
if (body.error) {
  // Many providers cap block ranges or result sizes. That is a limit, not an outage.
  console.error("getLogs error:", body.error.message);
} else {
  console.log("logs returned:", body.result.length);
}

If you see errors about block range or result size, that is a documented limit of the RPC method, not evidence that the endpoint is unreliable. The fix is to narrow the range or move the query to an indexer, not to switch providers.

Your next step

Take the probe script above, point it at two or three Optimism endpoints you are considering, and run it for an hour from the region your app runs in. Log the results. Compare error rate and p90 latency, not the single fastest response.

If you want to understand the tradeoffs between self-hosted and hosted RPC, or how indexing fits alongside RPC, browse the OnFinality Learn guides. If you are ready to compare endpoint options, start from the OnFinality Optimism network page and confirm everything with your own measurements.

The point is not to find a provider someone else recommends. The point is to have numbers you trust for your own workload.

  • Run the probe for a real window from your production region
  • Compare error rate and p90 across candidates
  • Keep the probe as a recurring check, not a one-time test