Polygon PoS reorgs are not simply shallower Ethereum reorgs: Bor rotates block producers per sprint, and finality is anchored by Heimdall checkpoints that land on Ethereum L1, so a block can be canonical on Bor yet not yet checkpoint-anchored. The reliable way to detect a reorg over RPC is to key on block hash rather than height, re-read the head each poll, and walk backward by parentHash until you reconnect with a stored canonical hash, reporting depth and fork point. Client choice matters because Bor (the current Polygon-recommended execution client after the Erigon-to-Bor migration) and legacy Erigon or cdk-erigon expose overlapping but not identical method surfaces, so an indexer that depends on a client-specific method can break when the endpoint is served by a different client. The safe design keys on the standard eth_* surface plus the checkpoint layer, and cross-checks a suspected reorg against Heimdall checkpoint state to distinguish a real reorg from a two-endpoint disagreement.
Polygon PoS Reorg Profile Versus Ethereum L1
On Ethereum L1, a single proposer is selected per slot, so a fork produced by one validator has a predictable shape: the reverted set is bounded by how many slots that proposer controlled before the next honest proposer extended a competing chain. Polygon PoS changes that shape because Bor rotates block producers per sprint rather than per slot. A span selects a validator subset, and within that span producers rotate each sprint, so the set of blocks a single producer can revert has a different boundary than the Ethereum L1 model. The Polygon documentation on Bor consensus describes sprints and spans and this producer rotation as the core of Bor's block production.
The second difference is finality anchoring. Heimdall periodically commits Bor state to Ethereum L1 as checkpoints, so a block can be canonical on Bor yet not yet checkpoint-anchored. That gap is the window in which a reorg can still occur. Practically, this means a Polygon indexer must hold two layers in mind at once: the Bor execution layer (blocks, receipts, state) and the Heimdall/checkpoint layer (finality anchors). Conflating 'the block is on Bor' with 'the block is finalised' is the root of most indexer bugs on Polygon. For the producer-rotation mechanics in more detail, see Polygon Bor sprints, spans and validator sets.
- Bor rotates producers per sprint within a span, so a single producer's fork has a different depth profile than an Ethereum L1 slot-based fork.
- Heimdall checkpoints anchor Bor state to Ethereum L1; until a block is checkpoint-anchored, it remains reorg-eligible.
- Treat 'canonical on Bor' and 'finalised' as separate states, not synonyms.
The Two-Layer Model: Bor Execution and Heimdall Checkpoints
The Bor execution layer is what a standard Ethereum JSON-RPC endpoint exposes: eth_getBlockByNumber, eth_getBlockByHash, eth_getBlockReceipts, eth_getLogs, and the trace modules where the client supports them. The Heimdall/checkpoint layer is a separate surface that reports which Bor blocks have been committed to Ethereum L1. These two layers are queried through different endpoints and different method namespaces, and they update on different cadences. An indexer that only reads the Bor layer can observe a block, index it, and later discover it was reorged out before any checkpoint anchored it.
The practical consequence is that your reorg detector should run on the Bor layer, but your finality confirmation should consult the checkpoint layer. When a suspected reorg appears, the checkpoint layer tells you whether the affected height was already anchored. If it was anchored, a two-endpoint disagreement is more likely than a real reorg; if it was not anchored, a real reorg is plausible. This cross-check is what separates a genuine reorg from an endpoint that is simply lagging or serving a stale head. The Ethereum block reorg detection over RPC article covers the L1 detector pattern that this Polygon-specific design extends.
- Bor layer: eth_* methods for blocks, receipts, logs, and traces.
- Heimdall/checkpoint layer: reports which Bor heights are anchored to Ethereum L1.
- Use the Bor layer to detect, the checkpoint layer to confirm.
Hash-Keyed Reorg Detection with parentHash Walking
The Ethereum JSON-RPC specification defines eth_getBlockByNumber and eth_getBlockByHash as returning a block whose parentHash links it to its predecessor, and both return null for an unknown block. That parentHash linkage is the primitive a reorg detector should use. Keying on block hash rather than height avoids the classic bug where a height is reused by a different block after a reorg, silently corrupting an index that stores height as a primary key. The specification reference for this behavior is the Ethereum JSON-RPC eth_getBlockByHash documentation.
A bounded detector works as follows: on each poll, re-read the head, compare its hash to the last stored canonical hash, and if they differ, walk backward by parentHash from the new head until you reconnect with a stored canonical hash. The height difference between the reconnect point and the old head is the reorg depth, and the reconnect block is the fork point. Bound the walk with a maximum depth so a pathological or misconfigured endpoint cannot make the detector loop indefinitely. The code below is a minimal Node.js detector using the standard eth_* surface.
const MAX_DEPTH = 128;
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 detectReorg(url, storedCanonical) {
// storedCanonical: Map<height, hash> of blocks you consider canonical
const head = await rpc(url, 'eth_getBlockByNumber', ['latest', false]);
if (!head) return { status: 'no-head' };
let cursor = head;
let depth = 0;
while (cursor && depth <= MAX_DEPTH) {
const height = parseInt(cursor.number, 16);
const known = storedCanonical.get(height);
if (known && known === cursor.hash) {
return {
status: depth === 0 ? 'no-reorg' : 'reorg',
depth,
forkPointHeight: height,
forkPointHash: cursor.hash,
newHead: head.hash
};
}
if (!cursor.parentHash || /^0x0+$/.test(cursor.parentHash)) break;
cursor = await rpc(url, 'eth_getBlockByHash', [cursor.parentHash, false]);
depth += 1;
}
return { status: 'unresolved', depth };
}
module.exports = { detectReorg };Client Landscape: Bor, Legacy Erigon, and cdk-erigon
Polygon has been moving from Erigon to Bor as the recommended execution client, and cdk-erigon exists for CDK chains. The first page of search results for Polygon reorg handling is dominated by client-migration and release-note surfaces, which tells you the reader's next question is 'which client, and what does that mean for my data'. Bor and legacy Erigon or cdk-erigon expose overlapping but not identical method surfaces and trace modules. An indexer that depends on a client-specific method will break when the endpoint is served by a different client, even if the chain data is identical.
The safe design keys on the standard eth_* surface plus the checkpoint layer, and treats client-specific extensions as 'documented / varies by client'. If you must use a trace or debug method, detect the client at startup and fail loudly rather than silently degrading. When you compare endpoints, the Multi-endpoint RPC consistency and head lag pattern applies: two endpoints can disagree on head without either being wrong, and that disagreement is not a reorg. For provider-level context on Polygon endpoints, see the Polygon RPC providers guide.
- Bor is the current Polygon-recommended execution client; legacy Erigon and cdk-erigon remain in the ecosystem.
- Method surfaces overlap but are not identical; trace and debug modules vary by client.
- Detect the client at startup and fail loudly on missing methods rather than degrading silently.
Checkpoint Cross-Check: Real Reorg Versus Endpoint Disagreement
A two-endpoint disagreement is not automatically a reorg. One endpoint may be lagging, serving a stale head, or temporarily partitioned. The checkpoint layer gives you a tiebreaker: if the affected height is already checkpoint-anchored on Heimdall, a real reorg at that height is implausible, and the more likely explanation is endpoint inconsistency. If the height is not yet anchored, a real reorg remains possible and you should trust the hash-keyed walk over any single endpoint's head.
The cross-check should be part of the detector's output, not a manual step. When the detector reports a reorg, record whether the fork point height was checkpoint-anchored at detection time. Over time, that field tells you whether your observed reorgs cluster in the unanchored window, which is exactly where the two-layer model predicts they should. This is also the field that distinguishes a genuine reorg from a provider-side head-lag artifact when you are comparing multiple endpoints.
- Anchored height + endpoint disagreement: suspect endpoint inconsistency, not a reorg.
- Unanchored height + hash mismatch: a real reorg is plausible.
- Record the anchored/unanchored flag on every detected reorg.
Reproducible Measurement: Filling a Results Table Against Your Endpoint
Because reorg behavior depends on your endpoint, your polling cadence, and the network conditions at the time, the only honest way to characterize it is to measure it yourself. Run the detector above against your own endpoint on a fixed interval, log every reorg event with its depth, fork point, and anchored flag, and aggregate over a window you choose. Do not rely on any published latency, throughput, or rate figure, including any that might appear in vendor material; measure against the endpoint you actually use.
The table below is a template. Fill the 'Observed' column from your own logs. The 'Documented behavior' column states what the protocol or client documentation supports, so you can tell a measurement from an assumption. Keep the window and polling interval fixed across rows so the comparison is meaningful.
To make the measurement concrete, the following curl command fetches the latest block from your endpoint. Run it on the same cadence as your detector and log the returned hash and number; comparing successive hashes at the same height is the simplest way to spot a reorg in raw RPC output. The expected output is a JSON object with fields including number (hex height), hash, and parentHash; if the call returns null, the endpoint has no head to serve and you should treat that as an endpoint problem rather than a reorg.
- Metric: observed reorg depth (blocks) — Documented behavior: bounded by producer rotation per sprint and by checkpoint anchoring; Observed: fill from your logs.
- Metric: fork point height relative to head — Documented behavior: parentHash walk reconnects at the fork point; Observed: fill from your logs.
- Metric: anchored flag at detection — Documented behavior: unanchored heights are reorg-eligible; Observed: fill from your logs.
- Metric: endpoint disagreement count — Documented behavior: not equivalent to a reorg; Observed: fill from your logs.
- Metric: detector unresolved rate — Documented behavior: bounded by MAX_DEPTH; Observed: fill from your logs.
curl -s -X POST https://your-polygon-rpc-endpoint \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["latest",false]}'Polling Cadence, Head Re-Reads, and Bounded Walks
The detector must re-read the head on every poll rather than caching it, because the head is the only reliable signal that a reorg may have occurred. Caching the head and comparing only heights will miss reorgs where the height is reused. The parentHash walk should be bounded by a maximum depth that you choose based on your risk tolerance and your observation window; if the walk exceeds the bound, report 'unresolved' rather than guessing. An unresolved result is a signal to inspect the endpoint, not a reorg.
Polling cadence interacts with reorg detection: a very slow poll can skip over a short reorg entirely, while a very fast poll increases load and may hit rate limits. There is no universal correct cadence, and any specific number would be a provider-specific or workload-specific choice. Choose a cadence, keep it fixed during a measurement window, and record it alongside your results so the numbers are interpretable. The OnFinality Learn hub collects related reliability patterns if you want to compare cadences across chains.
- Re-read the head every poll; never compare heights alone.
- Bound the parentHash walk and report 'unresolved' when the bound is exceeded.
- Fix the polling cadence during a measurement window and record it with results.
Limitations and Tradeoffs of Hash-Keyed Detection
Hash-keyed detection with parentHash walking is robust but not free. It requires storing a canonical hash per height, which grows with your retention window, and it requires an extra eth_getBlockByHash call per step of the walk. On a deep reorg the walk cost scales with depth, so the MAX_DEPTH bound is also a cost bound. If your retention window is short, you may reconnect quickly but lose the ability to detect deep reorgs; if it is long, you pay more storage and more walk steps.
The checkpoint cross-check adds a dependency on a second surface, which is a second thing that can fail or lag. Treat a checkpoint-layer failure as 'unknown', not as 'anchored' or 'unanchored'. Finally, client-specific methods are a portability risk: any detector that depends on a trace or debug extension is tied to the client that serves it. The standard eth_* surface plus the checkpoint layer is the portable subset, and it is the subset this design uses. For historical data needs that go beyond the reorg window, see Polygon archive nodes and historical RPC.
- Storage grows with retention window; walk cost grows with reorg depth.
- Checkpoint-layer failure should be treated as 'unknown', not as a finality signal.
- Client-specific methods reduce portability; prefer the standard eth_* surface.
Troubleshooting Common Reorg-Detection Failures
The most common failure is height-keyed indexing, where a reorg reuses a height and the index silently overwrites or duplicates data. The fix is to key on hash and store the height as an attribute, not a primary key. The second most common failure is treating a two-endpoint disagreement as a reorg; the fix is the checkpoint cross-check described above. The third is an unbounded parentHash walk that loops or times out on a misbehaving endpoint; the fix is MAX_DEPTH plus an 'unresolved' status.
A fourth failure is silent client drift: an endpoint that was serving Bor starts serving a different client, and a client-specific method disappears. The fix is startup client detection and a loud failure on missing methods. A fifth is stale head caching, where the detector compares against a cached head and misses reorgs; the fix is re-reading the head every poll. Each of these failures is observable in the results table if you log the right fields.
- Height-keyed index: switch to hash-keyed storage.
- Two-endpoint disagreement: add the checkpoint cross-check.
- Unbounded walk: add MAX_DEPTH and an 'unresolved' status.
- Client drift: detect the client at startup and fail loudly.
- Stale head: re-read the head every poll.
Next Steps: Hardening a Polygon Indexer
Start by running the detector against your own endpoint and filling the results table over a fixed window. Then add the checkpoint cross-check and the anchored flag to your logs, and re-run the window so you can compare. Once you have a baseline, decide your retention window and MAX_DEPTH based on the observed depths, not on an assumption. If you need provider-level guidance on Polygon endpoints, the Polygon RPC providers guide and the Polygon network page are the starting points.
For production, pair the detector with a multi-endpoint consistency check so a single lagging endpoint cannot masquerade as a reorg, and review RPC pricing and the API service if you are sizing endpoint capacity. The broader reliability patterns live in the OnFinality Learn hub, and the L1 detector pattern that this Polygon design extends is in Ethereum block reorg detection over RPC.
- Run the detector, fill the results table, then add the checkpoint cross-check.
- Choose retention window and MAX_DEPTH from observed depths, not assumptions.
- Pair the detector with multi-endpoint consistency checks for production.