Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Infrastructure & Operations12 min read

eth_syncing and Sync Status: Verifying a Production Ethereum RPC

Learn what eth_syncing actually returns, why false does not prove a node is usable, and how to build a trustworthy sync assertion for production Ethereum RPC.

TL;DR

eth_syncing returns false when a node is not syncing and an object with startingBlock, currentBlock, and highestBlock when it is, but false does not prove the node is usable or at the network head. Many clients report false while still healing state or syncing to a known head, so a health check built solely on eth_syncing is unsound. A reliable assertion combines eth_chainId and net_version to confirm the chain, then measures head lag by sampling eth_blockNumber from the candidate and independent references over a window and comparing deltas. This article provides a runnable Node.js checker and a results table to verify your own endpoint.

The eth_syncing Return-Value Contract

The Ethereum JSON-RPC specification defines eth_syncing as returning false when the node is not syncing and an object describing sync progress otherwise. That object typically includes startingBlock, currentBlock, and highestBlock, but the exact field set and its accuracy vary by client and by version. This is documented behavior, not a bug, and it means any parser must handle two very different shapes: a boolean and an object.

A naive client that treats false as healthy has asserted nothing about which chain state the node is serving. The node may be fully synced, or it may be far behind but not currently in a sync loop. The return value is a status flag, not a health verdict. For a production system, you need to know the node's position relative to the network head, not just whether it is actively syncing.

The JSON-RPC 2.0 specification governs the request and response envelope, including error handling, but it does not define eth_syncing semantics. The Ethereum JSON-RPC specification is the authoritative source for the method's return contract. Always consult the execution client's documentation for the fields it actually returns, because clients differ.

  • false: node reports it is not syncing; does not imply it is at the network head.
  • Object: node reports sync progress; fields vary by client and version.
  • Common fields: startingBlock, currentBlock, highestBlock (may be absent or stale).
  • Parsing must handle both boolean and object shapes without throwing.

Why eth_syncing Can Report False While a Node Is Behind

Snap sync and checkpoint sync allow a node to become operational before it has fully validated historical state. During state healing, the node may report false because it considers itself synced to the head it knows about, even though it is still catching up to the network head. This is a documented and long-reported client behavior, visible in public issue trackers and client documentation.

Clients also differ in what they consider 'synced'. Some report false once they have the latest block header, even if state is incomplete. Others may report false when they are within a small distance of the head. The result is that eth_syncing alone cannot distinguish a node that is fully caught up from one that is merely not in an active sync loop.

For a production RPC endpoint, the risk is false confidence. A load balancer may route to a backend that reports false but is minutes or hours behind. Your application then reads stale data without error. The only sound approach is to measure head lag directly, using eth_blockNumber comparisons against independent references.

  • Snap/checkpoint sync: node may be operational before full state validation.
  • State healing: node may report false while still catching up.
  • Client-specific definitions of 'synced' vary; do not assume uniformity.
  • False negatives are the primary failure mode for eth_syncing-based health checks.

Chain Identity Checks Before Height Comparison

Before comparing block heights, confirm the node is on the expected chain. Use eth_chainId to get the chain ID and net_version to get the network ID. These calls are cheap and should be part of every health check. A node on the wrong chain will have a different block height and could produce misleading lag measurements.

For Ethereum mainnet, the chain ID is 1. For testnets, it differs. If your application expects mainnet, a node on a testnet will report a much lower block number and appear lagging. Checking chain identity first prevents false positives and ensures your reference endpoints are on the same chain.

The RPC endpoints guide (RPC Assistant) covers how to select and verify endpoints. For OnFinality-specific network details, see the Ethereum network page.

  • eth_chainId returns the chain ID (e.g., 1 for mainnet).
  • net_version returns the network ID (often the same as chain ID).
  • Always compare candidate and reference on the same chain.
  • Mismatched chain IDs invalidate any height comparison.

Measuring Head Lag with eth_blockNumber Deltas

Head lag is the difference between the candidate node's latest block number and the network head. To measure it reliably, sample eth_blockNumber from the candidate and from at least two independent reference endpoints over a short window. Then compare the deltas: a node at the tip advances at the same rate as the references; a lagging node advances at the same rate but at a fixed offset; a stalled node does not advance.

A single comparison is not enough because block production is bursty. One-shot differences are noise. Use a window of several samples and compute the median difference rather than the maximum. The median stabilizes the signal and reduces the impact of outliers. A window of 5–10 samples over 30–60 seconds is usually sufficient for a production check.

The Detecting RPC node head lag and stale responses article covers client-side detection in more detail. For monitoring and alerting, see RPC node monitoring, metrics and alerts.

  • Sample candidate and references over a window (e.g., 5–10 samples).
  • Compute the difference at each sample: candidate height minus reference height.
  • Use the median difference as the lag estimate.
  • A stalled node shows zero advancement across samples.
  • A lagging node shows a consistent offset.
  • A healthy node shows a difference near zero (within a block or two).

Tag Semantics: latest, safe, and finalized

The tags latest, safe, and finalized come from different places for a single client, and a client may serve one correctly and another stale. latest refers to the most recent block the node knows about, which may not be the network head. safe and finalized refer to consensus-layer checkpoints and may lag behind latest. Health checks should state which tag they probe.

For production systems, comparing latest across candidate and references is the most direct measure of head lag. However, if your application relies on finalized data, you should also verify that the finalized block is advancing. A node can be at the head for latest but lagging on finalized if it is not receiving consensus updates.

Document which tag your health check uses and ensure your reference endpoints support the same tag. Mixing tags between candidate and reference will produce misleading lag numbers.

  • latest: most recent block known to the node; may not be network head.
  • safe: recent block considered safe by consensus; may lag latest.
  • finalized: block considered final; lags latest by at least two epochs.
  • State the tag in your health check and use it consistently.

Operational Limits of Remote RPC Endpoints

An RPC endpoint you do not operate can be load-balanced across multiple backends. Two consecutive probes may not hit the same node. This makes per-probe comparison the only safe pattern: compare the candidate's response to the reference's response at the same moment, rather than assuming a long-lived baseline. A baseline from an earlier probe may reflect a different backend.

If you operate your own node, you can maintain a baseline, but you should still sample over a window to account for bursty block production. For third-party endpoints, always treat each probe as independent. The API service and RPC pricing pages describe OnFinality's offerings, but the measurement method applies to any provider.

Load balancing also means that a single endpoint URL may return different heights on consecutive calls. Your checker should not assume monotonicity across probes. Instead, compare candidate and reference at the same sample index.

  • Remote endpoints may be load-balanced; consecutive probes may hit different backends.
  • Compare candidate and reference at the same moment, not against a historical baseline.
  • For self-operated nodes, a baseline is possible but still sample over a window.
  • Do not assume monotonic block heights across probes on a load-balanced endpoint.

Runnable Node.js Checker for Sync Assertion

The following Node.js script polls a candidate endpoint and two reference endpoints over a window, computes the median lag, and prints a verdict. It uses the native fetch API (Node.js 18+). Replace the placeholder URLs with your own endpoints. The script handles both boolean and object responses from eth_syncing for completeness, but the verdict is based on eth_blockNumber deltas.

Run it with: node sync-check.js. The script outputs a table of samples and a final verdict. Use the results to fill the table in the next section.

const CANDIDATE = 'https://your-candidate-rpc';
const REF1 = 'https://reference-1-rpc';
const REF2 = 'https://reference-2-rpc';
const SAMPLES = 7;
const INTERVAL_MS = 5000;

async function rpc(url, method, params = []) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(json.error.message);
  return json.result;
}

async function getBlockNumber(url) {
  const hex = await rpc(url, 'eth_blockNumber');
  return parseInt(hex, 16);
}

async function getChainId(url) {
  return rpc(url, 'eth_chainId');
}

function median(arr) {
  const sorted = [...arr].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}

(async () => {
  const chainId = await getChainId(CANDIDATE);
  console.log('Candidate chainId:', chainId);
  const refChainId = await getChainId(REF1);
  if (chainId !== refChainId) {
    console.error('Chain ID mismatch. Aborting.');
    process.exit(1);
  }

  const rows = [];
  for (let i = 0; i < SAMPLES; i++) {
    const [c, r1, r2] = await Promise.all([
      getBlockNumber(CANDIDATE),
      getBlockNumber(REF1),
      getBlockNumber(REF2)
    ]);
    const refMedian = median([r1, r2]);
    const lag = c - refMedian;
    rows.push({ sample: i + 1, candidate: c, ref1: r1, ref2: r2, refMedian, lag });
    console.log(`Sample ${i + 1}: candidate=${c} ref1=${r1} ref2=${r2} refMedian=${refMedian} lag=${lag}`);
    if (i < SAMPLES - 1) await new Promise(r => setTimeout(r, INTERVAL_MS));
  }

  const lags = rows.map(r => r.lag);
  const medianLag = median(lags);
  const maxLag = Math.max(...lags);
  const minLag = Math.min(...lags);
  const candidateAdvance = rows[rows.length - 1].candidate - rows[0].candidate;
  const refAdvance = rows[rows.length - 1].refMedian - rows[0].refMedian;

  console.log('\n--- Summary ---');
  console.log(`Median lag: ${medianLag}`);
  console.log(`Min lag: ${minLag}, Max lag: ${maxLag}`);
  console.log(`Candidate advance: ${candidateAdvance}, Reference advance: ${refAdvance}`);

  let verdict = 'UNKNOWN';
  if (candidateAdvance === 0 && refAdvance > 0) verdict = 'STALLED';
  else if (medianLag > 5) verdict = 'LAGGING';
  else if (Math.abs(medianLag) <= 2) verdict = 'HEALTHY';
  else verdict = 'DEGRADED';
  console.log(`Verdict: ${verdict}`);
})();

Results Table for Your Own Measurements

Use the table below to record your own measurements. Run the checker against your candidate endpoint and two references, then fill in the columns. The median lag and verdict columns should be computed from your samples. This table is a template; do not treat the example values as real measurements.

After filling the table, compare your results across different times of day and different endpoints. A healthy endpoint should show a median lag near zero and consistent advancement. A lagging endpoint will show a positive median lag. A stalled endpoint will show zero advancement.

  • Sample: sequential number of the probe.
  • Candidate: block number from your endpoint.
  • Reference 1 / Reference 2: block numbers from independent endpoints.
  • Reference median: median of the two reference heights.
  • Lag: candidate minus reference median.
  • Verdict: HEALTHY, DEGRADED, LAGGING, or STALLED based on your thresholds.

Limitations and Tradeoffs

This method measures head lag, not full sync state. A node can be at the head for latest but still healing state or missing historical data. For applications that require archive data, you must separately verify historical availability, as covered in Ethereum archive node and historical RPC.

Reference endpoints are not infallible. If both references are behind or on a different fork, your lag measurement will be wrong. Use at least two independent references and consider a third for critical systems. The method also assumes block production is ongoing; on a testnet with no recent blocks, lag measurements may be meaningless.

The checker uses eth_blockNumber, which returns the latest block number. It does not verify that the block is canonical or that state is available. For a complete health check, combine this with eth_syncing parsing, chain ID verification, and application-level read tests. The OnFinality Learn hub has related guides on monitoring and detection.

  • Measures head lag, not full sync or state availability.
  • Requires at least two independent references.
  • Assumes active block production on the chain.
  • Does not verify canonicality or historical data.
  • Combine with application-level read tests for full coverage.

Troubleshooting Common Sync Assertion Failures

If your checker reports a large lag but the node appears healthy in other tools, verify that all endpoints are on the same chain. A chain ID mismatch will produce a consistent offset. Also check that you are comparing the same tag; if the candidate uses latest and the reference uses finalized, the lag will be large and expected.

If the checker reports STALLED, confirm that the reference endpoints are advancing. If the references are also stalled, the chain may be halted or your references may be down. If only the candidate is stalled, the node may be disconnected from peers or experiencing a consensus issue. Check the node's peer count and logs.

If the checker reports HEALTHY but your application sees stale data, the issue may be state healing or archive data availability, not head lag. Use eth_getBlockByNumber with a recent block to verify state, and consider a dedicated archive endpoint. The Base OP-Stack node sync status guide covers similar concepts for OP-Stack chains.

  • Chain ID mismatch: verify eth_chainId on all endpoints.
  • Tag mismatch: ensure candidate and references use the same tag.
  • References stalled: check reference health and chain status.
  • Candidate stalled: check peer count and node logs.
  • Healthy head but stale data: check state healing and archive availability.

Next Steps for Production Sync Monitoring

Integrate the checker into your monitoring pipeline. Run it on a schedule (e.g., every minute) and alert on median lag exceeding a threshold. Store the results to track trends over time. Combine with eth_syncing parsing for a complete picture, but do not rely on eth_syncing alone.

For OnFinality-specific endpoints, see the Ethereum network page and RPC pricing. The API service provides managed RPC endpoints that you can verify with this method. For more guides, visit the OnFinality Learn hub.

Remember that the exact field set and accuracy of eth_syncing vary by client and version. Always test against your specific client and version. The method described here is client-agnostic and relies on eth_blockNumber, which is widely supported.

  • Schedule the checker and alert on median lag thresholds.
  • Store results for trend analysis.
  • Combine with eth_syncing parsing and chain ID checks.
  • Test against your specific client and version.
  • Review related guides for monitoring and detection.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started