A Base OP-Stack node learns about new blocks through two distinct paths: the sequencer feed, a pub/sub stream that delivers unsafe blocks immediately, and L1 derivation, which reconstructs the safe chain from batches and state roots posted to Ethereum. The unsafe head from the feed can differ from the safe head derived from L1, and both are exposed through op-node RPC methods such as optimism_syncStatus or the Engine API forkchoice state. Applications that trust the unsafe head for value-bearing actions risk reorgs, because only the safe and finalized heads carry L1-backed guarantees. This article explains the data-flow model, shows runnable Node.js and curl examples for reading heads and reconnecting the feed, and provides a reproducible results table for measuring unsafe-to-safe gaps on your own endpoint.
The OP-Stack Data-Flow Model: Sequencer, Feed, and Derivation
In the OP-Stack model documented at docs.optimism.io/stack/rollup/overview, a sequencer orders transactions and gossips the resulting blocks. Consumer op-nodes connect to the sequencer feed, a pub/sub stream commonly carried over WebSocket or HTTP, to receive these blocks immediately as unsafe blocks. In parallel, the same node derives the safe chain from sequencer batches and state roots that are posted to L1.
This dual-path design means a node always has at least two views of the chain: the fast, sequencer-provided unsafe head and the slower, L1-derived safe head. The OP Stack Rollup Node specification describes the sequencer feed and op-node behavior in detail, including how a non-sequencer consumer connects to a sequencer endpoint.
For operators, the practical consequence is that the feed is a latency optimization, not a trust anchor. The feed tells you what the sequencer is producing right now; derivation tells you what L1 has attested to. Both are needed for a complete picture, and the Base OP Stack finality, safe and finalized blocks article covers how those heads are tagged over RPC.
- Sequencer: orders transactions and gossips blocks to the feed.
- Feed: pub/sub stream delivering unsafe blocks to consumer op-nodes.
- Derivation: reconstructs the safe chain from L1 batches and state roots.
- Unsafe head: latest block seen from the feed, subject to reorg.
- Safe head: L1-derived block equivalent to the L2 safe tag.
- Finalized head: L1-finalized block, the strongest guarantee.
Why the Unsafe Head and Safe Head Diverge
The unsafe head advances as soon as the sequencer publishes a block, while the safe head advances only when the corresponding batch is included in an L1 block and processed by derivation. This timing difference is the primary reason the two heads diverge. Under normal operation the unsafe head leads the safe head by some number of blocks; during L1 congestion or sequencer restarts, the gap can widen.
A divergence is not necessarily an error. It is the expected behavior of a rollup that separates fast sequencing from L1-backed settlement. The risk arises when an application treats the unsafe head as if it were safe. Because the unsafe head can be reorged, any value-bearing action based solely on it can be invalidated. The Base OP-Stack reorg depth and indexer rollback article discusses how to reason about rollback depth for indexers.
To read both heads, you query the op-node rollup or admin RPC. The optimism_syncStatus method returns the unsafe, safe, and finalized L2 block numbers along with their L1 origins, giving you a single snapshot of the node's view.
- Unsafe head leads because the feed is faster than L1 inclusion.
- Safe head lags by the time it takes to post and derive batches.
- Finalized head lags further, waiting on L1 finality.
- A widening gap is a signal to check L1 health and sequencer status.
Reading Unsafe, Safe, and Finalized Heads with optimism_syncStatus
The op-node exposes optimism_syncStatus over its RPC interface. A single call returns the current unsafe, safe, and finalized L2 block numbers, their L1 origins, and the node's sync state. This is the most direct way to observe the feed-versus-derivation relationship on a running node.
The example below uses curl against a local op-node RPC endpoint. Replace the URL with your own op-node RPC address. The response shape is documented in the OP Stack Rollup Node specification and is stable across OP-Stack chains, including Base.
If you are connecting through a hosted endpoint rather than a local op-node, check which methods are exposed. The Base RPC endpoint (RPC Assistant) page describes the standard Base RPC surface, while op-node-specific methods like optimism_syncStatus are typically available only on your own node or a provider that explicitly exposes them.
curl -s -X POST http://localhost:9545 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"optimism_syncStatus","params":[]}' \
| jq '{unsafe: .result.unsafe_l2.number, safe: .result.safe_l2.number, finalized: .result.finalized_l2.number, unsafe_l1: .result.unsafe_l2.l1origin.number, safe_l1: .result.safe_l2.l1origin.number}'The JSON Shape of a Sequencer-Feed Block Payload
The sequencer feed delivers block payloads as JSON messages over a pub/sub transport. The exact envelope varies by client and transport, but the payload generally contains the execution payload fields: parent hash, block number, state root, timestamp, transactions, and the L1 origin metadata. Understanding this shape helps you parse feed messages and compare them against derived blocks.
The example below shows a representative payload structure. Field names follow the Engine API execution payload conventions used by op-node. Treat this as a structural reference; always validate against the actual messages your node emits, because provider-specific framing can differ.
When you consume the feed directly, you are responsible for tracking the unsafe head yourself. The node's own optimism_syncStatus remains the authoritative source for the safe and finalized heads, which the feed does not carry.
{
"parentHash": "0xabc...",
"blockNumber": "0x112a880",
"stateRoot": "0xdef...",
"timestamp": "0x66f1a2c0",
"transactions": ["0x02f8..."],
"l1Origin": {
"blockNumber": "0x14a2b3c",
"blockHash": "0x123..."
}
}Subscribing to the Feed and Reconnecting with Backoff
A consumer op-node connects to a sequencer endpoint to receive the feed. When the feed disconnects or the node restarts, the node falls back to L1 derivation and must catch up. An application that watches only the feed will see a gap during this window, because the feed does not replay missed blocks. The correct recovery pattern is to reconnect with exponential backoff and then reconcile against optimism_syncStatus to detect any gap.
The Node.js example below implements a reconnect loop with backoff and a reconciliation step. It uses a WebSocket connection to the feed and periodically polls optimism_syncStatus over HTTP. Replace the endpoints with your own. The pattern is transport-agnostic; the same logic applies to HTTP long-polling feeds.
After reconnecting, compare the last block number you saw from the feed against the unsafe head reported by optimism_syncStatus. If the node's unsafe head is ahead, you missed blocks and should backfill from the node's RPC rather than assuming the feed will resend them.
const WebSocket = require('ws');
const fetch = require('node-fetch');
const FEED_URL = 'ws://localhost:8546';
const RPC_URL = 'http://localhost:9545';
let lastSeen = 0;
let backoff = 1000;
async function syncStatus() {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'optimism_syncStatus', params: [] })
});
const json = await res.json();
return json.result;
}
function connect() {
const ws = new WebSocket(FEED_URL);
ws.on('open', () => { backoff = 1000; console.log('feed connected'); });
ws.on('message', (data) => {
const block = JSON.parse(data);
lastSeen = parseInt(block.blockNumber, 16);
console.log('unsafe block', lastSeen);
});
ws.on('close', async () => {
const status = await syncStatus();
const nodeUnsafe = parseInt(status.unsafe_l2.number, 16);
if (nodeUnsafe > lastSeen) {
console.log('gap detected: missed', nodeUnsafe - lastSeen, 'blocks; backfill required');
}
setTimeout(connect, backoff);
backoff = Math.min(backoff * 2, 30000);
});
ws.on('error', (err) => console.error('feed error', err.message));
}
connect();Choosing the Right Head for Indexers and Bridges
The head you trust should match the value at risk. For display-only data such as a block explorer's latest-block counter, the unsafe head is acceptable because a reorg only changes the display. For indexing that feeds analytics, the safe head is usually the right choice because it is L1-derived and equivalent to the L2 safe block tag. For bridges and any action that moves value, the finalized head is the only head that carries L1-finality guarantees.
This tiering is consistent with the head model described in the OP Stack documentation. The Base OP Stack node sync status and the Engine API article explains how the Engine API forkchoice state maps to these heads, which is useful when you are driving an execution client directly.
A practical rule: never finalize a withdrawal, credit a deposit, or release an escrow based on the unsafe head. Wait for the finalized head, and use the safe head as an intermediate checkpoint for non-value state.
- Unsafe head: UI counters, mempool-style monitoring, non-value telemetry.
- Safe head: indexing, analytics, non-value state that tolerates rare rollback.
- Finalized head: bridges, withdrawals, deposits, escrow, any value transfer.
- Always record the L1 origin alongside the L2 block for auditability.
Flashblocks and the Derivation Model: Different Signals
Flashblocks are sub-second preconfirmations that sit on top of the OP-Stack model. They are a latency-oriented signal designed to give applications an early view of pending state, not a replacement for safe or finalized derivation. Confusing flashblocks with derivation leads to incorrect assumptions about finality.
The Base OP-Stack L1 derivation and timestamps article covers how derivation timestamps are read over RPC, which is the correct way to reason about when a block became safe. Flashblocks do not change the derivation timeline; they only shorten the time to a preliminary signal.
Treat flashblocks as an optimization layer for user experience. Keep your safe and finalized logic anchored to derivation and L1 finality, and use flashblocks only where a reorg-tolerant, low-latency signal is acceptable.
- Flashblocks: sub-second preconfirmations, latency-oriented, reorg-tolerant.
- Derivation: L1-backed safe head, the basis for indexing checkpoints.
- Finality: L1-finalized head, the basis for value-bearing actions.
- Do not substitute flashblocks for safe or finalized checks.
Reproducible Results Table for Your Own Endpoint
Because feed behavior, derivation lag, and reconnect timing depend on your node, network conditions, and L1 health, the only reliable numbers are the ones you measure. Use the table below to record observations from your own endpoint. Run the syncStatus query and the feed subscriber together, and log the values at a fixed interval.
Fill in each row with a timestamp, the unsafe and safe L2 block numbers, their L1 origins, the computed gap, and any reconnect events. Repeat across at least one L1 congestion period and one node restart to capture the interesting cases. This gives you a baseline you can compare against after configuration changes.
Do not treat any single measurement as a benchmark. The goal is a reproducible method, not a published number. If you need provider-level performance data, request it from your provider rather than inferring it from a single run.
- Timestamp: ISO 8601 time of the sample.
- Unsafe L2 block: from optimism_syncStatus or the feed.
- Safe L2 block: from optimism_syncStatus.
- Finalized L2 block: from optimism_syncStatus.
- Unsafe L1 origin: L1 block backing the unsafe head.
- Safe L1 origin: L1 block backing the safe head.
- Unsafe-to-safe gap: unsafe minus safe block number.
- Time-to-safe: wall-clock time from unsafe observation to safe observation.
- Feed reconnect count: number of reconnect events in the interval.
Limitations and Tradeoffs of Feed-Based Consumption
The sequencer feed is a sequencer-provided service. Its availability, framing, and retention are controlled by the sequencer operator, not by the protocol. Self-hosted feed consumption requires running an op-node; you cannot consume the feed meaningfully without one, because the feed alone does not give you the safe or finalized heads.
Trusting the unsafe head for value-bearing actions is unsafe because it can be reorged. The feed does not replay missed blocks, so a disconnect creates a gap that must be backfilled from the node's RPC. Running your own op-node adds operational cost but removes dependence on a third-party feed endpoint and gives you direct access to optimism_syncStatus.
For teams that prefer not to run infrastructure, a managed endpoint can expose the standard Base RPC surface. See Base RPC endpoint (RPC Assistant) and RPC pricing for options, and API service for managed access patterns. Note that op-node-specific methods may not be available on all managed endpoints; verify before designing around them.
- Feed availability is controlled by the sequencer operator.
- Feed does not replay missed blocks; backfill is your responsibility.
- Self-hosted consumption requires an op-node.
- Unsafe head is reorg-prone and unsuitable for value actions.
- Managed endpoints may not expose op-node-specific RPC methods.
Troubleshooting Feed and Derivation Issues
Most feed problems fall into a few categories: connection failures, silent gaps, and head divergence. Start by confirming the op-node is running and that optimism_syncStatus returns a coherent snapshot. If the unsafe head is advancing but the safe head is stalled, the issue is usually on the L1 derivation side, not the feed.
If the feed disconnects repeatedly, check network stability and the sequencer endpoint's reachability. Increase backoff caps to avoid reconnect storms. If you see a gap after reconnect, backfill from the node's RPC rather than waiting for the feed to resend.
If the safe head is far behind the unsafe head, check L1 gas prices and batch posting. Derivation lag is often an L1 economics problem. The Base OP Stack node sync status and the Engine API article covers how to inspect sync state through the Engine API when the rollup RPC is not enough.
- Feed connected but no messages: verify subscription topic and transport.
- Unsafe advancing, safe stalled: investigate L1 derivation and batch posting.
- Repeated disconnects: check network, endpoint reachability, backoff caps.
- Gap after reconnect: backfill from node RPC, do not wait for replay.
- Heads inconsistent across nodes: compare L1 origins and sync status.
Next Steps for Base Node Operators
Start by running an op-node and querying optimism_syncStatus to establish a baseline for your unsafe, safe, and finalized heads. Then add a feed subscriber with reconnect and backoff, and log the results table described above. This gives you a reproducible view of how your node behaves under normal and degraded conditions.
For network-level context, see the Base network page and the OnFinality Learn hub for related deep-dives. If you are designing an indexer, read the Base OP-Stack reorg depth and indexer rollback article next, and pair it with the Base OP Stack finality, safe and finalized blocks guide for RPC-level head handling.
Finally, decide your trust tier per operation: unsafe for display, safe for indexing, finalized for value. Document that decision in your runbook, and revisit it whenever you change endpoints or providers.
- Run an op-node and baseline optimism_syncStatus.
- Add a feed subscriber with reconnect and backoff.
- Log the results table across normal and degraded periods.
- Define trust tiers per operation and document them.
- Review provider method coverage before designing around op-node RPC.