Ethereum

Ethereum

Block Management Best Practices

In development you fetch a block, log it, and move on. Production is a different animal. You're processing every one of the ~7,200 blocks Ethereum produces per day without gaps, surviving node hiccups, and staying correct when the head reorgs out from under you. The cost of a missed block or a double-counted reorg isn't a console warning — it's drifted balances and support tickets. That's why the rigor here goes well past the happy-path code you'd write for a quick script.

The patterns below are the ones that hold up against sustained mainnet traffic: retrying failed reads without losing data, detecting reorgs by watching parentHash, processing wide block ranges without exhausting memory, and failing over to a second provider when the primary degrades. Each comes with a checklist you can lift straight into a readiness review before you point real users at https://ethereum.therpc.io/YOUR_API_KEY.

Error Handling and Recovery

  • Bounded retries with growing backoff. Wrap each eth_getBlock in a retry loop of 3 to 5 attempts with delays that double from a fraction of a second. Since a fresh block arrives every 12 seconds, treat a block that's still missing after a few seconds as genuinely absent rather than retrying indefinitely against an empty result.
  • Make timeouts non-destructive. Keep a persisted cursor of the last block number you fully committed. If a read times out, the worker can crash and restart without skipping a height — it resumes from the cursor and refetches, so a network blip never silently swallows a block.
  • Detect reorgs by parent hash and roll back. On each new block, compare its parentHash to the hash you stored for the previous height. If they differ, the chain reorganized: walk back to the common ancestor, undo any state derived from the orphaned blocks, then replay the new canonical ones. Never mutate finalized-block-derived state — only the unfinalized tail can move.
  • Configure a fallback node for continuity. A second endpoint takes over when the primary returns sustained errors or stalls. Verify the standby reports eth_chainId of 0x1 and agrees on the hash at a recent shared height before you trust its data, so failover doesn't quietly hand you a forked or lagging view.

Performance Optimization

  • Cache finalized blocks, expire the rest. Blocks at or below finalized are immutable — store them keyed by hash with no expiry. The unfinalized tail between finalized and latest can still change, so cap that cache to a short window (a couple of slots) sized to your reorg tolerance, not to RAM convenience.
  • Batch to amortize round trips. A single JSON-RPC batch covering a contiguous range replaces dozens of individual eth_getBlockByNumber calls. The win is mostly in connection and request overhead, which dominates on Ethereum's dense ranges where each block carries hundreds of transactions.
  • Right-size polling. When you poll the head, tie the interval to the 12-second slot and stop polling harder when the chain hasn't moved. Every poll spends Compute Units; polling faster than blocks are produced buys nothing but cost.
  • Stream large ranges, don't buffer them. Backfilling thousands of blocks, process in fixed batches (10 to 50) and release each batch before fetching the next. Holding a year of fully-hydrated Ethereum blocks in memory at once will OOM the worker — a bounded sliding window keeps memory flat regardless of range size.

Security Considerations

  • Scale your finality bar to the value. On post-Merge Ethereum the meaningful safety line isn't a raw confirmation count — it's the finalized tag, reached after roughly two epochs (~12.8 minutes), beyond which a revert would require slashing a third of staked ETH. Low-value or reversible actions can act on safe or a handful of blocks; settlement-grade flows should wait for finalized.
  • Check integrity before you process. Confirm each block's parentHash chains to the prior stored block, and that its number is exactly one above it. A gap or a mismatch means either a reorg or a node fault — handle it before you derive any state.
  • Avoid replay confusion. EIP-155 ties signed transactions to chainId 1, so a mainnet transaction can't be replayed on Sepolia and vice versa. Validate the chainId on anything you ingest, and only act on a transaction once its inclusion is confirmed by receipt at a finalized height, never from a mempool sighting.
  • Treat anomalies as a tripwire. Backward-moving timestamps, two providers disagreeing on the block hash at one height, an unexpected parentHash gap, or a sudden cluster of orphaned heads all point to a reorg or a misbehaving node. Pause irreversible processing and alert rather than pushing the suspect data through.

Implementation Examples

import Web3 from 'web3';
const web3 = new Web3('https://ethereum.therpc.io/YOUR_API_KEY');
// Robust block fetching with retries
const fetchBlockWithRetry = async (blockNumber, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
const block = await web3.eth.getBlock(blockNumber);
if (block) return block;
throw new Error('Block not found');
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
}
}
};
// Efficient block monitoring
const createBlockMonitor = (callback, errorHandler) => {
let lastProcessedBlock = 0;
return web3.eth
.subscribe('newBlockHeaders')
.on('data', async (blockHeader) => {
try {
if (blockHeader.number <= lastProcessedBlock) return;
await callback(blockHeader);
lastProcessedBlock = blockHeader.number;
} catch (error) {
errorHandler(error);
}
})
.on('error', errorHandler);
};
// Memory-efficient block processing
const processBlockRange = async (startBlock, endBlock, processor) => {
const batchSize = 10;
for (let i = startBlock; i <= endBlock; i += batchSize) {
const batch = await Promise.all(
Array.from(
{ length: Math.min(batchSize, endBlock - i + 1) },
(_, index) => fetchBlockWithRetry(i + index)
)
);
await processor(batch);
}
};
// Handle block reorgs
const monitorReorgs = async (callback) => {
let lastBlock = await web3.eth.getBlock('latest');
return web3.eth.subscribe('newBlockHeaders').on('data', async (block) => {
if (block.parentHash !== lastBlock.hash) {
await callback({
oldBlock: lastBlock,
newBlock: block,
});
}
lastBlock = block;
});
};

Development Checklist

  • Type the block shape. Ethereum returns block fields as hex strings — number, timestamp, gasLimit, baseFeePerGas all come back as 0x-prefixed quantities. TypeScript types around your parsing layer catch the classic bug of comparing a hex string as if it were a decimal number, and document which fields exist post-Merge (an empty uncles array, sha3Uncles as the empty-list hash) versus pre-Merge.
  • Cover every failure branch. Error handling should account for the block being null (not yet produced), the call timing out, a 429 from rate limits, and a parentHash mismatch. Each needs a distinct response, not a single catch-all that swallows them.
  • Instrument from the start. Log the block number and hash on every processed block, emit a metric for head lag in seconds, and count reorgs by depth. You want this wired before launch, not bolted on during an incident.
  • Test the unhappy paths. Unit tests should cover a missing block, a simulated reorg where parentHash doesn't match, a batch where one read fails, and the resume-from-cursor restart. Happy-path block fetching rarely breaks; the reorg and recovery code is where regressions hide.

Production Checklist

  • Alert on the metrics that mean something. Head lag beyond a few slots, the latest-to-finalized gap drifting past its normal ~2-epoch span, RPC error rate, reorg depth, and processing throughput against the ~12-second block cadence. These tell you the pipeline is falling behind or the chain is misbehaving.
  • Wrap node calls in a circuit breaker. When a provider crosses an error threshold, trip the breaker so you stop hammering a failing node and shed load to the fallback. This keeps a single degraded upstream from cascading into your own database and queue backlog.
  • Run at least two providers. A single endpoint is a single point of failure. Two independent providers, both pinned to chainId 1, let you keep ingesting blocks during a regional outage or a provider's maintenance window, and let you cross-check head hashes when something looks off.
  • Health-check continuously. Verify the node's head timestamp tracks wall clock within a few slots, that eth_chainId returns 0x1, and that your cursor is advancing. A node can answer requests while quietly stuck — only a freshness check catches that.
  • Define degradation behavior in advance. When the primary slows, route to the standby; when both struggle, back off polling, widen retry budgets, and keep serving reads from finalized-block cache rather than failing user requests outright.

Common Pitfalls to Avoid

  • Thin error handling drops blocks silently. A single uncaught timeout that crashes a worker without a persisted cursor leaves a hole in your block history that nothing flags. The data looks complete until a balance reconciliation says otherwise.
  • No reorg handling corrupts state. If you commit credits or event-derived state off latest and never check parentHash, a routine head reorg leaves you having acted on a block that no longer exists on the canonical chain. On Ethereum these short reorgs are normal before finality, so this isn't an edge case — it's a matter of when.
  • Blind spots become outages. Without head-lag and reorg metrics, a node that fell behind or a provider serving a forked view goes unnoticed until users report wrong data. By then you're debugging in production with no signal.
  • Untested recovery code rots. The retry, fallback, and reorg-rollback paths run rarely, so without tests they break unnoticed and fail exactly when you need them. A pipeline that's never exercised its failure branches is fragile by construction.

See also

Ready to call this in production?

Free tier covers personal projects. Pay-as-you-go scales without a card.