Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides12 min read

Base OP-Stack Finality: Safe, Finalized, and Latest Block Tags over RPC

Understand Base OP-Stack finality stages and how to use safe, finalized, and latest block tags in JSON-RPC calls.

TL;DR

On Base, an OP-Stack L2, the JSON-RPC block tags 'latest', 'safe', and 'finalized' represent distinct stages of finality. 'latest' is the sequencer head and can reorg, 'safe' is committed to an L1 block that is unlikely to reorg, and 'finalized' is irreversible. Use 'finalized' for critical reads like withdrawals or indexer watermarks, and 'safe' for most application logic that requires stability.

Direct Answer: What Do Safe, Finalized, and Latest Mean on Base?

When you query a Base JSON-RPC endpoint, the block tags latest, safe, and finalized do not all point to the same block. latest is the head of the chain as seen by the sequencer and may be reorged. safe is a block whose batch has been submitted to an L1 block that is old enough that it is considered safe from reorgs. finalized is a block that is fully confirmed by L1 finality and the OP-Stack derivation process, making it irreversible for practical purposes. This article explains the mechanism behind these stages and how to use them correctly in your applications.

For authoritative details, refer to the Base documentation on transaction lifecycle and the Optimism documentation on transaction finality. These are primary sources that describe the exact semantics and current behavior.

How OP-Stack Architecture Creates Finality Stages

Base is an OP-Stack (Optimism) L2. The sequencer proposes blocks and periodically submits batches of transactions to an L1 contract. The L1 contract, along with the dispute-game and output-root system, determines when an L2 block is considered safe or finalized. This process is asynchronous: the sequencer produces blocks quickly, but finality on L1 takes time.

The three head values exposed by a Base node are:

  • latest: The most recent block the sequencer has produced. This block is not yet anchored to L1 and can be reorged if the sequencer reorgs or if a dispute occurs.

  • safe: A block whose batch has been committed to an L1 block that is old enough that it is unlikely to reorg. This is the practical 'safe to build on' marker for most applications.

  • finalized: A block that has been fully confirmed by L1 finality and the canonical OP-Stack derivation. This is irreversible for practical purposes.

The exact depth and timing of these stages are documented behavior, not fixed constants. They depend on L1 finality and the OP-Stack configuration. As a developer, you should not assume a fixed number of blocks or seconds; instead, query the node for the current safe and finalized heads.

Querying Safe, Finalized, and Latest with JSON-RPC

You can query each head using the standard eth_getBlockByNumber method with the block tag parameter. The block tag can be a string like 'latest', 'safe', or 'finalized', or an EIP-1898 block number object. For example, to get the latest block: eth_getBlockByNumber('latest', false). To get the safe block: eth_getBlockByNumber('safe', false). To get the finalized block: eth_getBlockByNumber('finalized', false).

The block numbers returned for safe and finalized will typically be behind latest. The difference is not a fixed constant; it varies based on L1 conditions and the sequencer's batching schedule. You can measure the difference in your environment using the script below.

Using latest for irreversible reads is dangerous. For example, if you are building an indexer that records the latest block as a watermark, a reorg could cause you to process a block that is later discarded. Similarly, if you are constructing a withdrawal proof, you must use a block that is finalized on L1, not just the latest L2 block.

Reproducible Node.js Script to Measure Finality Frontier

The following script connects to a Base endpoint, fetches the latest, safe, and finalized block numbers, and prints the differences. It also re-samples the safe head over time to observe how the finality frontier advances. Replace YOUR_BASE_ENDPOINT with your actual endpoint URL.

Run the script with Node.js (v18 or later). It uses the built-in fetch API, so no external dependencies are required.

const endpoint = 'YOUR_BASE_ENDPOINT';

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

async function getHeads() {
  const latest = parseInt(await rpc('eth_blockNumber', []), 16);
  const safe = parseInt((await rpc('eth_getBlockByNumber', ['safe', false])).number, 16);
  const finalized = parseInt((await rpc('eth_getBlockByNumber', ['finalized', false])).number, 16);
  return { latest, safe, finalized };
}

async function main() {
  console.log('Sampling Base finality heads...');
  const samples = [];
  for (let i = 0; i < 5; i++) {
    const heads = await getHeads();
    samples.push(heads);
    console.log(`Sample ${i+1}: latest=${heads.latest}, safe=${heads.safe}, finalized=${heads.finalized}, behind_safe=${heads.latest - heads.safe}, behind_finalized=${heads.latest - heads.finalized}`);
    await new Promise(resolve => setTimeout(resolve, 5000)); // wait 5 seconds
  }
  console.log('\nFill in the table below with your observed values:');
  console.log('| Sample | latest | safe | finalized | behind_safe | behind_finalized |');
  console.log('|--------|--------|------|-----------|-------------|------------------|');
  samples.forEach((s, i) => {
    console.log(`| ${i+1} | ${s.latest} | ${s.safe} | ${s.finalized} | ${s.latest - s.safe} | ${s.latest - s.finalized} |`);
  });
}

main().catch(err => { console.error(err); process.exit(1); });

Expected Output and Fill-in Results Table

When you run the script, you will see output similar to the following (actual numbers vary by network conditions and endpoint):

Record your own observations in the table below. The values are environment-specific and will change over time. This method lets you verify the finality behavior on your chosen endpoint.

  • | Sample | latest | safe | finalized | behind_safe | behind_finalized |
  • |--------|--------|------|-----------|-------------|------------------|
  • | 1 | | | | | |
  • | 2 | | | | | |
  • | 3 | | | | | |
  • | 4 | | | | | |
  • | 5 | | | | | |
Sample 1: latest=12345678, safe=12345670, finalized=12345660, behind_safe=8, behind_finalized=18

Which Head Should You Use When?

Choosing the right block tag depends on your use case. Here is a decision guide:

  • Use finalized for: irreversible operations such as constructing withdrawal proofs, finalizing cross-domain transfers, or recording permanent indexer watermarks. This ensures you never build on a block that could be reorged.

  • Use safe for: most application logic that requires a stable view of the chain, such as reading account balances, executing smart contract calls that depend on state, or building transactions that should not be reorged. safe is the recommended default for reads that need to be consistent.

  • Use latest for: real-time monitoring, user-facing transaction status that can tolerate reorgs, or when you need the most recent block and understand the reorg risk.

For critical financial operations, always prefer finalized or safe over latest. For example, if you are building a bridge that locks assets on Base, you should wait for finalized before considering the deposit irreversible.

Common Pitfalls and How to Avoid Them

Developers often make mistakes when dealing with L2 finality. Here are the most common pitfalls and fixes:

  • Treating latest as final: This is the most common error. latest can reorg. Always use safe or finalized for anything that must be permanent.
  • Assuming a fixed reorg depth: The difference between latest and safe is not a constant. It depends on L1 conditions and sequencer batching. Measure it in your environment rather than hardcoding a value.
  • Using L2 receipts for cross-domain finality: A transaction receipt on Base does not mean the transaction is final on Ethereum. Cross-domain withdrawals require the Optimism proof window, which is longer than L2 finality. Refer to the Optimism documentation on cross-domain communication for details.
  • Ignoring the staggered advancement: safe and finalized do not advance in lockstep. safe typically advances faster than finalized. Your application should handle both independently.
  • Not using EIP-1898 for specific blocks: If you need to query a specific block number, use the EIP-1898 object format, e.g., { blockNumber: '0x...' }, to avoid ambiguity.

Limitations and Tradeoffs

While safe and finalized provide stronger guarantees, they come with tradeoffs. safe and finalized blocks are behind latest, so using them means your application sees a slightly delayed view of the chain. For most use cases, this delay is acceptable, but for real-time applications like price feeds or live dashboards, you may need to use latest and handle reorgs gracefully.

Additionally, the exact semantics of safe and finalized may evolve as the OP-Stack changes. Always refer to the official documentation for the latest behavior. The Base documentation and Optimism documentation are the authoritative sources.

Finally, note that the behavior described here is specific to OP-Stack L2s like Base. Other L2s (e.g., Arbitrum, zkSync) have different finality models. Do not assume the same block tags mean the same thing across networks.

Next Steps and Further Reading

Now that you understand Base finality, you can apply this knowledge to build more robust applications. For more details on Base RPC usage, explore the following resources:

  • API service - Learn about OnFinality's API offerings.

Never Worry about Infrastructure Again

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

Get Started