Ethereum block reorgs occur when the chain a node follows switches to a different branch, invalidating previously canonical blocks. Detecting them from a single RPC endpoint requires keying on block hashes and verifying parent-hash linkage, not trusting block numbers as identities. The Ethereum JSON-RPC specification defines eth_getBlockByNumber and eth_getBlockByHash, both returning a block object with a parentHash field that links it to its predecessor. A bounded detector keeps a rolling window of recent (height, hash, parentHash) rows, re-reads the head each poll, and walks backward until the stored hash matches the observed parentHash, reporting the number of replaced blocks as reorg depth. Safe and finalized tags offer stronger guarantees but are consensus-layer judgments, not absolute promises. Any observed reorg counts are a sample of that endpoint's view, not the network's true reorg rate.
The Reorg Mechanism and Why Block Numbers Are Not Identities
A blockchain reorganization, or reorg, happens when a node switches from one chain branch to another that has more accumulated work or, in proof-of-stake Ethereum, greater attestation weight. Blocks that were canonical under the old branch become non-canonical, and any state derived from them may be invalid. The Ethereum JSON-RPC specification describes blocks as objects with a parentHash field that cryptographically links each block to its predecessor, forming a chain. A block number is merely a label for a position in that chain, not a unique identity. Two different blocks can occupy the same height on competing branches.
Because block numbers are positional labels, a detector that stores only the latest block number and assumes the block at that height is stable will miss reorgs entirely. The correct approach is to treat the block hash as the identity and verify that the hash is still reachable from the current head by walking parent-hash links. This is the core principle behind reliable reorg detection and is independent of any specific RPC provider. For a broader introduction to Ethereum RPC methods, see the RPC endpoints guide (RPC Assistant).
- A block is canonical only while the chain the node follows contains it.
- Block numbers are positions, not identities; hashes are identities.
- Parent-hash linkage is the primary signal for detecting a chain switch.
- Derived state below the fork point becomes invalid after a reorg.
Parent-Hash Linkage as the Primary Detection Signal
The Ethereum JSON-RPC specification states that eth_getBlockByNumber returns a block object for a given height, and eth_getBlockByHash returns a block object for a given hash, or null if the block is unknown to the node. Both objects include a parentHash field. To detect a reorg, re-request the stored hash for height N and compare it with the parentHash of the block now at height N+1. If the hash stored for N no longer appears as the parent of N+1, the chain has switched. This comparison is the fundamental reorg signal.
A subtle but critical point: eth_getBlockByHash returns the block if the node still has it, whether or not it is canonical. A hash that resolves is not necessarily a hash that is canonical. The canonical test always requires walking back from the head. This distinction is documented in the Ethereum JSON-RPC specification for eth_getBlockByHash and is essential for correct detector logic.
- Compare stored hash at height N with parentHash of block at N+1.
- A resolvable hash is not proof of canonicality.
- Canonicality is established only by walking back from the head.
- Both eth_getBlockByNumber and eth_getBlockByHash return null for unknown blocks.
Safe and Finalized Tags: Documented Guarantees and Their Limits
The Ethereum JSON-RPC specification defines block tags including latest, safe, finalized, and pending. The finalized tag is the strongest guarantee a node can offer and is the appropriate anchor for irreversible settlement. The safe tag is a weaker intermediate that may still be subject to reorgs under certain network conditions. Both are consensus-layer judgments that a node exposes based on its view of the chain, not absolute promises that no reorg can ever occur at that depth.
Relying solely on finalized for all operations can be impractical for high-frequency applications because finality takes time. Many systems use a confirmation depth policy based on observed reorg statistics rather than waiting for finality. The tradeoff is between settlement speed and safety. For a detailed discussion of how finality interacts with reorgs, see the Ethereum JSON-RPC specification for eth_getBlockByNumber.
- finalized is the strongest tag but still a consensus-layer judgment.
- safe is weaker and may be reorged under some conditions.
- Confirmation depth policies balance speed against safety.
- Provider behavior for these tags is documented but varies by provider.
Building a Bounded Reorg Detector with a Rolling Window
A practical reorg detector maintains a rolling window of recent (height, hash, parentHash) rows. On each poll, it re-reads the head block and walks backward through the stored window, comparing the stored hash at each height with the parentHash of the block at the next height. The walk stops when the stored hash matches the observed parentHash. The number of blocks replaced is the reorg depth, and the height where the walk stopped is the fork point.
This bounded approach avoids unbounded memory growth and keeps the detector responsive. The window size should be at least as deep as the confirmation policy you intend to enforce. For a complementary perspective on reconciling blocks and transactions, see Block-by-block EVM indexer reconciliation.
- Keep a rolling window of (height, hash, parentHash) rows.
- Re-read the head each poll and walk backward until hashes match.
- Reorg depth = number of blocks replaced; fork point = height where walk stopped.
- Window size should exceed your intended confirmation depth.
Runnable Node.js Detector with Per-Poll Output
The following Node.js script connects to a single Ethereum JSON-RPC endpoint, polls the head block, and detects reorgs by parent-hash linkage. It emits per-poll output including head number, head hash, observed depth, fork point height, and blocks replaced. It also maintains a cumulative log so you can accumulate your own measured reorg statistics. Replace the RPC_URL with your endpoint from OnFinality's Ethereum network page.
The script uses eth_getBlockByNumber with the latest tag to fetch the head, and eth_getBlockByHash to retrieve blocks by hash when walking backward. It stores a rolling window of the last WINDOW_SIZE blocks. On each poll, it compares the stored hash at each height with the parentHash of the block at the next height. If a mismatch is found, it reports the reorg depth and fork point.
const { ethers } = require('ethers');
const RPC_URL = 'https://your-endpoint.onfinality.io'; // replace with your endpoint
const WINDOW_SIZE = 64; // number of recent blocks to track
const POLL_INTERVAL_MS = 12000; // ~1 block time
const provider = new ethers.JsonRpcProvider(RPC_URL);
// Rolling window: array of { height, hash, parentHash }
let window = [];
let cumulativeReorgs = [];
async function fetchBlockByNumber(height) {
const block = await provider.send('eth_getBlockByNumber', [
'0x' + height.toString(16),
false
]);
if (!block) return null;
return {
height: parseInt(block.number, 16),
hash: block.hash,
parentHash: block.parentHash
};
}
async function fetchBlockByHash(hash) {
const block = await provider.send('eth_getBlockByHash', [hash, false]);
if (!block) return null;
return {
height: parseInt(block.number, 16),
hash: block.hash,
parentHash: block.parentHash
};
}
async function poll() {
const head = await fetchBlockByNumber('latest');
if (!head) {
console.log('Failed to fetch head block');
return;
}
// Add head to window if new
if (window.length === 0 || window[window.length - 1].height < head.height) {
window.push(head);
if (window.length > WINDOW_SIZE) window.shift();
}
// Walk backward to find fork point
let forkPoint = null;
let blocksReplaced = 0;
for (let i = window.length - 1; i > 0; i--) {
const stored = window[i - 1];
const current = window[i];
if (stored.hash !== current.parentHash) {
// Mismatch: reorg detected
forkPoint = stored.height;
blocksReplaced = window.length - i;
break;
}
}
if (forkPoint !== null) {
console.log(`REORG DETECTED: head=${head.height} hash=${head.hash}`);
console.log(` fork point height: ${forkPoint}`);
console.log(` blocks replaced: ${blocksReplaced}`);
cumulativeReorgs.push({ timestamp: Date.now(), forkPoint, blocksReplaced });
// Rewind window to fork point
window = window.filter(b => b.height <= forkPoint);
} else {
console.log(`No reorg. head=${head.height} hash=${head.hash}`);
}
console.log(`Cumulative reorgs observed: ${cumulativeReorgs.length}`);
}
setInterval(poll, POLL_INTERVAL_MS);
poll();Measuring Reorg Depth and Fork Point Distribution
Running the detector over a long observation period produces a distribution of fork points and reorg depths. This distribution is the empirical basis for choosing a confirmation depth. A short observation window yields only a sample of that endpoint's view, not an estimate of the network's true reorg rate. To make claims about network-wide reorg frequency, you need a long observation period and ideally multiple independent endpoints.
Use the following results table template to record your own measurements. Fill it in with data from your detector. Do not rely on quoted figures from blog posts; measure against your own endpoint. For a method to compare endpoints, see Multi-endpoint RPC consistency and head lag.
- Results Table columns: Observation period, Endpoint, Total polls, Reorgs observed, Max depth, Median depth, Fork point heights.
- Record the timestamp and block height for each reorg event.
- Compare distributions across multiple endpoints if possible.
- A full distribution requires a long observation period, not a short sample.
Choosing a Confirmation Depth from Observed Data
The confirmation depth policy should be derived from the observed fork-point distribution, not copied from a blog post. If your detector observes that 99% of reorgs have a depth of 2 or fewer blocks over a long period, a depth of 12 blocks provides a wide margin. However, the choice also depends on the value at risk and the cost of delayed settlement. A deeper confirmation policy slows settlement but reduces the probability of acting on a reorged block.
For irreversible settlement, the finalized tag is the strongest anchor. For high-frequency operations, a depth-based policy may be more practical. The key is to base the decision on your own measurements and to document the assumptions. For a related discussion on detecting lag, see Detecting an RPC node behind the chain tip.
- Derive confirmation depth from observed fork-point distribution.
- Consider value at risk and settlement speed tradeoffs.
- Use finalized for irreversible settlement when possible.
- Document assumptions and re-evaluate periodically.
Indexer Remediation: Rewind and Re-index After a Reorg
For indexers, a reorg invalidates derived state below the fork point. A detector that only logs reorgs is insufficient; the remediation path must be designed before it fires. The standard approach is to rewind the indexer to the fork point and re-index forward from there. This requires that the indexer can identify the fork point and that it has a way to roll back derived state, such as database transactions or versioned records.
The remediation path should be tested regularly. If the indexer cannot rewind, it may serve stale or incorrect data after a reorg. For a detailed treatment of reconciliation, see Block-by-block EVM indexer reconciliation. Bulk retrieval methods like eth_getBlockReceipts: bulk receipts in one call can speed up re-indexing.
- Reorg invalidates derived state below the fork point.
- Remediation: rewind to fork point, re-index forward.
- Design and test the remediation path before it is needed.
- Use bulk retrieval methods to speed up re-indexing.
Limitations, Tradeoffs, and Provider Variability
No single RPC endpoint provides a complete view of the network. Any reorg counts observed are a sample of that endpoint's view and may not reflect the network's true reorg rate. Provider-specific behavior for block tags, caching, and head propagation varies. The Ethereum JSON-RPC specification defines the protocol semantics, but implementation details are documented per provider and may vary.
A deeper confirmation policy increases safety but slows settlement. A shallower policy speeds settlement but increases reorg risk. The optimal depth depends on the application's risk tolerance and the observed reorg distribution. There is no universal safe number. Additionally, the detector itself adds load to the endpoint; polling frequency and window size should be tuned to balance detection latency against resource usage. For pricing considerations, see RPC pricing.
- Single-endpoint observations are samples, not network-wide truths.
- Provider behavior for tags and caching varies; check documentation.
- Deeper confirmation slows settlement; shallower increases risk.
- Detector polling adds load; tune frequency and window size.
Troubleshooting Common Detector Issues
If the detector reports no reorgs but you suspect one occurred, verify that the window size is large enough to cover the reorg depth. A reorg deeper than the window will not be detected because the stored hashes have already been shifted out. Also check that the endpoint is not caching responses; some providers cache eth_getBlockByNumber for a short period, which can mask reorgs. Use eth_getBlockByHash for hash-addressed retrieval to avoid cache ambiguity.
If the detector reports frequent false positives, ensure that you are comparing the correct fields. The parentHash of the block at height N+1 should match the hash of the block at height N. A mismatch in height numbering or a race condition between polls can cause spurious results. Also verify that the endpoint is fully synced; a lagging node may serve stale blocks. For help with endpoint selection, see the API service and the OnFinality Learn hub.
- Window too small: deep reorgs missed.
- Provider caching can mask reorgs; use eth_getBlockByHash.
- False positives: check field comparisons and height numbering.
- Lagging node: verify sync status before trusting results.
Next Steps: Operationalizing Reorg Detection
To operationalize reorg detection, integrate the detector into your monitoring stack and alert on reorg events. Record every reorg with timestamp, fork point, and depth in a persistent store. Over time, this data informs your confirmation depth policy. Consider running detectors against multiple endpoints to compare views and detect provider-specific anomalies.
For production systems, combine reorg detection with a robust remediation path. Test the rewind and re-index procedure regularly. Use the finalized tag for irreversible settlement where possible. For endpoint options, explore OnFinality's Ethereum network page and review RPC pricing to choose a plan that fits your polling frequency. The RPC endpoints guide (RPC Assistant) provides additional context on endpoint selection.
- Integrate detector into monitoring and alert on reorgs.
- Persist reorg events for long-term distribution analysis.
- Run detectors against multiple endpoints for comparison.
- Test remediation procedures regularly.