Monitor Moonbeam RPC Uptime and Health from a Node.js Script

Build a small Node.js 18+ health-check script that distinguishes Moonbeam RPC reachability from indexing/query latency, logs failures with context, and helps you measure uptime yourself instead of trusting a dashboard.

If you run anything on Moonbeam, "is the RPC up?" is the wrong question on its own. You need to know whether the endpoint is reachable, whether it is returning correct chain data, and whether the slowness you feel is coming from RPC access or from an indexer/query layer sitting in front of it. This guide gives you one concrete task: write a Node.js 18+ script that probes a Moonbeam RPC endpoint on an interval, records success/failure with timing, and exits with a clear signal so you can wire it into your own monitoring. You will supply your own endpoint URL — never hardcode one from a tutorial.

Separate RPC access from indexing before you measure anything

RPC access is a request/response call to a node: you send a JSON-RPC method like eth_blockNumber or eth_chainId and the node answers from its current state. Indexing is a different layer: a service ingests chain data into a database and answers structured queries, often with lag relative to the chain head. A healthy RPC endpoint can sit behind a slow indexer, and a fast indexer can be fed by an RPC endpoint that is currently degraded. If you conflate the two, your uptime numbers will be meaningless.

For a Moonbeam health check, start with the narrowest possible RPC probe: ask for the chain ID and the latest block number. Those are cheap, deterministic calls that tell you the endpoint is alive and on the network you expect. Only after that baseline is stable should you add any indexed query, and when you do, measure it separately so you can attribute slowness correctly.

If you are choosing where to point the probe, the Moonbeam network page describes the network and the access options available; use it to understand what you are connecting to rather than as a source of a hardcoded URL.

  • RPC probe = JSON-RPC method against a node (chain ID, block number).
  • Indexed query = structured read against an indexer/database, which can lag the chain head.
  • Measure them as two separate signals so you can attribute failures and latency.

Write the Node.js 18+ probe

The script below uses the built-in fetch available in Node.js 18+, so there are no dependencies. It reads the endpoint from an environment variable, sends a JSON-RPC POST, validates the response shape, and records elapsed time. It treats a non-2xx HTTP status, a JSON-RPC error object, and a malformed body as distinct failure modes so your logs tell you what actually broke.

Run it with MOONBEAM_RPC_URL set to your own endpoint. Do not paste an endpoint from a blog post into production; use the one your provider issued to you.

JavaScript · Node.js
// health-check.mjs — Node.js 18+
const RPC_URL = process.env.MOONBEAM_RPC_URL;
const EXPECTED_CHAIN_ID = process.env.EXPECTED_CHAIN_ID ?? null; // e.g. "0x504" for Moonbeam

if (!RPC_URL) {
  console.error("Missing MOONBEAM_RPC_URL");
  process.exit(2);
}

async function rpc(method, params = []) {
  const started = performance.now();
  let res;
  try {
    res = await fetch(RPC_URL, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
      signal: AbortSignal.timeout(8000),
    });
  } catch (err) {
    return { ok: false, kind: "network", method, error: String(err) };
  }

  const elapsedMs = Math.round(performance.now() - started);

  if (!res.ok) {
    return { ok: false, kind: "http", method, status: res.status, elapsedMs };
  }

  let body;
  try {
    body = await res.json();
  } catch {
    return { ok: false, kind: "bad-json", method, elapsedMs };
  }

  if (body.error) {
    return { ok: false, kind: "rpc-error", method, elapsedMs, error: body.error };
  }
  if (body.result === undefined) {
    return { ok: false, kind: "missing-result", method, elapsedMs };
  }
  return { ok: true, method, elapsedMs, result: body.result };
}

const chain = await rpc("eth_chainId");
const block = await rpc("eth_blockNumber");

if (!chain.ok) {
  console.error("chainId probe failed:", chain);
  process.exit(1);
}
if (EXPECTED_CHAIN_ID && chain.result.toLowerCase() !== EXPECTED_CHAIN_ID.toLowerCase()) {
  console.error(`Wrong chain: got ${chain.result}, expected ${EXPECTED_CHAIN_ID}`);
  process.exit(1);
}
if (!block.ok) {
  console.error("blockNumber probe failed:", block);
  process.exit(1);
}

console.log(JSON.stringify({
  chainId: chain.result,
  blockNumber: parseInt(block.result, 16),
  chainIdMs: chain.elapsedMs,
  blockNumberMs: block.elapsedMs,
}, null, 2));

Expected result: a JSON object with chainId, a decimal blockNumber, and two timing values. A successful run exits 0; any probe failure exits 1 with a structured reason; a missing URL exits 2.

Failure handling is deliberate: timeouts surface as kind: "network", a 429 or 5xx surfaces as kind: "http" with the status, and a JSON-RPC error surfaces as kind: "rpc-error" with the provider's message. That distinction is what lets you tell rate limiting apart from an outage apart from a bad request.

Turn the probe into an uptime measurement you can trust

A single run tells you almost nothing about uptime. Uptime is a ratio over a window, so you need repeated samples. Run the script on a schedule — cron, a systemd timer, or your platform's scheduler — and append each result as one JSON line to a log file. Then compute the ratio yourself over whatever window matters to you.

This is the honest way to answer "what is Moonbeam RPC uptime?" for your setup: you measure it against your endpoint, from your network, on your schedule. Published numbers from any provider describe their own vantage point and their own definition of success, which may not match yours. If you want a starting point for how providers frame these measurements, the RPC Assistant is a reasonable place to read about endpoint selection and health considerations before you commit to a monitoring design.

Keep the block-number probe in the loop even when the chain-ID probe passes. A node that answers eth_chainId but returns a block number that stops advancing is reachable but not healthy, and that pattern is exactly what a naive ping check misses.

  • Sample on a fixed interval and store one JSON line per run.
  • Compute success ratio over a window you define; do not borrow someone else's window.
  • Track block-number progression, not just reachability, to catch stalled nodes.
  • Attribute latency to the RPC probe separately from any indexed query you add later.
bash
# Run every minute, append one JSON line per sample
* * * * * MOONBEAM_RPC_URL="$MOONBEAM_RPC_URL" EXPECTED_CHAIN_ID="0x504" \
  node /opt/health/health-check.mjs >> /var/log/moonbeam-health.jsonl 2>&1

# Rough success ratio over the last 1440 samples (24h at 1/min)
tail -n 1440 /var/log/moonbeam-health.jsonl \
  | grep -c '"chainId"' \
  | awk '{ printf "success ratio: %.3f\n", $1/1440 }'

The ratio is only as good as your sample cadence and your definition of success. Document both next to the log so the number stays interpretable months later.

Where to go next

Once the RPC baseline is stable, add one indexed query and log it to a separate file. Compare the two timelines when something feels slow: if RPC timings are flat but indexed queries spike, the problem is in the query layer, not the node. That single habit resolves most "the RPC is down" reports before they become incidents.

For background on how RPC access and indexing fit together in a SubQuery-style stack, browse the OnFinality Learn library. It is a better next read than another provider comparison table, because it explains the layers you are now measuring.

  • Keep RPC and indexed-query logs separate so you can compare timelines.
  • Revisit your expected chain ID whenever you change networks.
  • Read the Learn library before adding more probes; understand the layer first.