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

Solana getBlocks and Skipped Slots: Gap-Free Block Indexing

A deterministic method for walking Solana slot ranges with getBlocks, classifying skipped slots, missing blocks, and retention gaps without treating every null as an error.

TL;DR

Solana slot numbers are not block numbers: slots advance on a fixed schedule, but a slot can be skipped when its leader does not produce a block, so a slot range is an upper bound on blocks, never an equality. The getBlocks method returns only confirmed slots that contain a block, in descending order, which means a numeric gap between returned slot numbers is evidence of skipped slots rather than an API bug. A gap-free indexer walks the range in chunks, records the set of returned slots, and classifies every slot as block present, skipped, or unresolved, where unresolved means the walk could not prove either state. Commitment levels and retention windows affect what resolves, so a reproducible historical walk pins a commitment and finishes below the finalized head. Chunk size, retention window, and per-call limits are provider and cluster properties, documented and varying by provider, not protocol constants.

Slot Numbers and Block Numbers Are Different Identifiers

On EVM chains, block height is a dense sequence: every increment corresponds to a produced block, so latestBlock - fromBlock + 1 equals the number of blocks in a range. Solana breaks that assumption. Slots are scheduled time units assigned to leaders, and a slot can be skipped when its leader does not produce a block. The official Solana getBlocks method reference documents that the method returns confirmed slots that contain blocks, not every slot in the requested span.

The practical consequence is that end_slot - start_slot + 1 is an upper bound on the number of blocks in a range, never an equality. An indexer that assumes density will either under-count blocks or misclassify legitimate gaps as data loss. If you are coming from an EVM reconciliation workflow, the mental model in block-by-block EVM indexer reconciliation does not transfer directly; Solana requires a slot-aware walk.

  • A slot is a scheduled opportunity to produce a block; a block is the produced artifact.
  • A skipped slot has no block and is expected behavior, not an error.
  • end_slot - start_slot + 1 is an upper bound on blocks, not a count.
  • getBlocks returns only slots that contain a block, in descending order.

Documented getBlocks Semantics and Descending Order

The official reference documents start_slot, end_slot, and commitment as parameters, with an inclusive range and a descending array of confirmed slot numbers that contain blocks. Because the array is descending, the lowest returned slot is the natural cursor for the next page when walking backward, and the highest returned slot tells you where the current chunk actually begins.

A contiguous run of returned slot numbers with a numeric gap between them is evidence of skipped slots, not an API defect. For example, a response containing 100, 99, 97, 96 indicates slot 98 was skipped. The gap is the signal you want to record, not suppress. The RPC endpoints guide covers how endpoint selection and provider behavior can affect which slots resolve.

  • Bounds are inclusive: both start_slot and end_slot are considered.
  • Results are descending, so the lowest returned slot is the next exclusive end_slot.
  • A numeric gap between returned slots is evidence of a skipped slot.
  • A full page is not guaranteed; never assume the chunk size was returned.

Why a Naive Per-Slot getBlock Loop Fails

A loop such as for (let s = start; s <= end; s++) getBlock(s) is wrong in three specific ways. First, it treats a null as a fatal error, when a skipped slot legitimately has no block and the getBlock reference documents that a skipped slot returns null. Second, it burns one request per slot even for gaps, which is wasteful across large ranges. Third, it cannot distinguish a null caused by a skipped slot from a null caused by retention limits or the wrong commitment level.

The correct approach is to use getBlocks to discover which slots contain blocks, then call getBlock only for those slots. This reduces request volume and makes the classification explicit. The same discipline of separating discovery from retrieval appears in Solana getSignaturesForAddress pagination, where signature discovery and transaction retrieval are distinct steps.

  • Null from getBlock is ambiguous without context: skipped, outside retention, or wrong commitment.
  • Per-slot loops waste requests on slots that will never have blocks.
  • Discovery via getBlocks should precede retrieval via getBlock.
  • Ambiguity must be recorded as unresolved, not silently dropped.

A Gap-Detection Algorithm for Slot Ranges

The algorithm walks the requested range in chunks, records the set of returned slots, and classifies every slot in the requested range into one of three states: block present, skipped, or unresolved. Block present means getBlocks returned the slot. Skipped means the slot falls inside a chunk that returned successfully but the slot was absent from the response. Unresolved means the walk could not prove either state, for example because the chunk failed, the endpoint returned an error, or the slot is near the retention boundary.

Unresolved slots must be recorded rather than silently dropped. A gap-free indexer is not one that reports zero gaps; it is one that accounts for every slot in the requested range with a defensible classification. This is the same principle behind querying Solana historical data over RPC, where retention boundaries change what can be proven.

  • Block present: getBlocks returned the slot in a successful chunk.
  • Skipped: the slot was absent from a successful chunk response.
  • Unresolved: the chunk failed or the slot is near retention limits.
  • Every slot in the requested range must receive exactly one classification.

Runnable Node.js Walker with Per-Chunk Summary

The following Node.js script walks a slot range in chunks, calls getBlocks, and prints a per-chunk summary as a table. It uses a configurable chunk size and commitment, and it records unresolved slots when a chunk fails. Replace the endpoint URL with your own provider endpoint; the numbers it prints are for you to measure against your own endpoint.

The script does not assume a full page. It records the lowest returned slot and uses it as the next exclusive end_slot, which is correct for the descending order documented by the getBlocks reference.

const ENDPOINT = process.env.SOLANA_RPC_URL || 'https://your-endpoint.example.com';
const COMMITMENT = 'finalized';
const CHUNK_SIZE = 1000;

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 json = await res.json();
  if (json.error) throw new Error(JSON.stringify(json.error));
  return json.result;
}

async function walkRange(startSlot, endSlot) {
  const rows = [];
  let cursor = endSlot;
  while (cursor >= startSlot) {
    const chunkStart = Math.max(startSlot, cursor - CHUNK_SIZE + 1);
    const span = cursor - chunkStart + 1;
    let returned = [];
    let unresolved = 0;
    try {
      returned = await rpc('getBlocks', [chunkStart, cursor, { commitment: COMMITMENT }]);
    } catch (err) {
      unresolved = span;
    }
    const returnedSet = new Set(returned);
    let skipped = 0;
    for (let s = chunkStart; s <= cursor; s++) {
      if (!returnedSet.has(s)) skipped++;
    }
    rows.push({
      requestedSpan: `${chunkStart}-${cursor}`,
      spanSize: span,
      returnedCount: returned.length,
      lowestReturned: returned.length ? Math.min(...returned) : null,
      detectedGaps: skipped,
      unresolved
    });
    if (returned.length === 0) break;
    cursor = Math.min(...returned) - 1;
  }
  return rows;
}

(async () => {
  const rows = await walkRange(250000000, 250010000);
  console.table(rows);
})();

Results Table for Your Own Endpoint Measurements

Run the walker against your own endpoint and fill the table below with your measurements. The values are not protocol constants; they depend on your provider, your cluster, and the commitment level you pin. State explicitly that chunk size, retention window, and per-call limits are provider and cluster properties, documented and varying by provider.

Use the table to compare endpoints or commitment levels. If unresolved counts are high, the range may be near retention or the endpoint may be rate-limiting. If detected gaps are unexpectedly high, verify that you are not mixing commitment levels across chunks.

  • Requested span: the inclusive slot range for the chunk.
  • Returned count: number of slots getBlocks returned.
  • Lowest returned: the next exclusive end_slot for the walk.
  • Detected gaps: slots absent from a successful chunk.
  • Unresolved: slots the walk could not classify.

Commitment Levels and the Finalized Head

Confirmed and finalized slots can differ for the newest chunk, because commitment reflects different levels of cluster agreement. A reproducible historical walk should pin a commitment and finish the range below the finalized head, so that the classification is stable across runs. The Solana commitment levels and confirmation guide explains how these levels relate to confirmation.

If you walk the newest slots with confirmed commitment, a slot that appears skipped may later resolve as a block is produced or as the cluster advances. For indexing, pinning finalized commitment and ending below the finalized head avoids this ambiguity. Document the commitment you used alongside your results table.

  • Pin one commitment for the entire walk.
  • Finish the range below the finalized head for reproducibility.
  • Confirmed and finalized can differ for the newest chunk.
  • Record the commitment used with your measurements.

Retention Boundaries and Outside-Retention Classification

Old slots eventually stop resolving because the endpoint no longer retains them. A slot that returns no block because it is outside retention must be recorded as outside retention, not as skipped. Conflating the two corrupts your gap statistics and can hide real data loss.

Retention windows are provider and cluster properties, documented and varying by provider. Before walking a historical range, confirm the retention window for your endpoint. The Solana network page and the API service describe how OnFinality structures access, while the RPC pricing page covers plan-level considerations. For a broader treatment of historical access, see querying Solana historical data over RPC.

  • Outside retention is a distinct classification from skipped.
  • Retention windows vary by provider and cluster.
  • Confirm retention before walking historical ranges.
  • Record outside-retention slots explicitly in your output.

Limitations and Tradeoffs of Slot-Range Walking

Chunked walking reduces request volume compared to per-slot getBlock loops, but it still requires one getBlocks call per chunk. Larger chunks reduce call count but increase the chance of hitting per-call limits, which are provider and cluster properties. Smaller chunks are more resilient but slower.

The classification is only as good as the endpoint's responses. If an endpoint returns errors for a chunk, those slots become unresolved, and the walk cannot prove their state. There is no protocol-level guarantee that every slot in a range will be classifiable in a single pass. The OnFinality Learn hub collects related guides on indexing and RPC behavior.

  • Larger chunks reduce calls but risk per-call limits.
  • Smaller chunks are resilient but slower.
  • Unresolved slots are a legitimate outcome, not a failure.
  • No single pass guarantees full classification.

Troubleshooting Common Gap-Detection Failures

If your walk reports many gaps, first check whether you are mixing commitment levels across chunks. A confirmed chunk followed by a finalized chunk can produce apparent gaps that are actually commitment differences. Second, check whether the range extends beyond retention; outside-retention slots will appear as gaps if you do not classify them separately.

If your walk reports many unresolved slots, check for rate limiting or endpoint errors. Reduce chunk size and retry failed chunks. If a chunk consistently fails, record it as unresolved and continue; do not silently drop it. The RPC endpoints guide covers endpoint selection and failover considerations.

  • Mixed commitment levels can create apparent gaps.
  • Outside-retention slots must be classified separately.
  • Rate limiting increases unresolved counts.
  • Retry failed chunks; never silently drop them.

Next Steps for Production Indexers

For production indexing, persist the classification for every slot in the requested range, including unresolved and outside-retention states. Schedule re-walks for unresolved ranges and for slots near the finalized head. Pin a commitment and document it alongside your results.

If you are building on OnFinality, review the Solana network page and the API service for endpoint options, and consult RPC pricing for plan-level details. For related indexing patterns, see Solana getSignaturesForAddress pagination and block-by-block EVM indexer reconciliation.

  • Persist every slot classification, including unresolved.
  • Re-walk unresolved ranges on a schedule.
  • Pin and document your commitment level.
  • Review provider retention and per-call limits before scaling.

Never Worry about Infrastructure Again

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

Get Started