Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Integration & Development14 min read

Solana getSignaturesForAddress Pagination Deep Dive

Walk a Solana address's signature history without gaps, duplicates, or silent truncation by handling cursors, commitment, and retention correctly.

TL;DR

getSignaturesForAddress returns signature-level status entries ordered newest-first, not full transactions, so a complete history walk is two-phase: enumerate signatures with before/until cursors, then fetch each transaction. The per-call maximum page size and the retention window are provider-and-cluster properties, not fixed protocol constants, so a correct walker must treat short pages, empty pages, and missing transactions as expected boundary conditions rather than errors. This guide explains the cursor mechanics, a gap-free algorithm, commitment interactions, WebSocket transport behavior, and a runnable Node.js example that produces reproducible evidence on your own endpoint.

What getSignaturesForAddress Returns and Omits

The Solana JSON-RPC method getSignaturesForAddress takes an address plus optional before, until, limit, commitment, and minContextSlot parameters, and returns an array of signature status entries ordered newest-first. Each entry carries signature, slot, err, memo, blockTime, and confirmationStatus. This is documented behavior in the official Solana getSignaturesForAddress RPC method reference.

Critically, the method deliberately omits the transaction itself. You get a signature and its status metadata, but not instructions, logs, or account keys. A complete history walk is therefore two-phase: first enumerate signatures, then fetch each transaction with getTransaction or a similar method. The second phase must tolerate a signature whose transaction is no longer retrievable, because nodes may prune older transaction data even when the signature index still lists it; the Solana getTransaction reference documents this retrieval behavior and its retention caveats.

This two-phase design matters for indexers because the phases have different failure modes. Signature enumeration is cheap and cursor-driven; transaction retrieval is heavier, can time out, and can return null for a signature that the node no longer serves. Treating a null transaction as a fatal error will stall a walk that is otherwise healthy.

  • Returns: signature, slot, err, memo, blockTime, confirmationStatus.
  • Omits: the transaction body, logs, and instruction data.
  • Implication: plan for a separate getTransaction phase with its own retry and skip policy.

How the before and until Cursors Actually Work

Both before and until take a signature, not a slot or an index. The practical pattern is to set before to the last signature of the previous page, which tells the node to return entries strictly older than that signature. You advance until only as a bounded stop condition, such as a known checkpoint signature you do not want to cross.

The most common silent truncation bug is reusing a stale until across pages. If you set until once at the start and never update it, every subsequent page is filtered against that same boundary, and the walk stops early without an error. The correct approach is to leave until unset for a full walk, or to advance it deliberately only when you intend to stop at a specific point.

Because before is a signature, not a slot, you must record the last signature received, not the last slot. Slots are not unique per address; multiple signatures for the same address can share a slot. Signatures are unique, which makes them the only safe cursor.

  • before: return entries strictly older than this signature.
  • until: stop when this signature is reached; use as a bounded stop, not a fixed filter.
  • Cursor type: signature, never slot.

Three Ways the Naive Pagination Loop Fails

First, using limit as a page size while assuming it always returns a full page. The limit parameter is a maximum, not a guarantee. A short page can mean the address simply has fewer remaining signatures, or it can mean the requested window fell outside retention. If your loop only stops on an empty page, a short page followed by an empty page can hide a retention boundary.

Second, restarting the walk from a saved slot instead of a saved signature. Because slots are not unique per address, resuming from a slot can skip or duplicate signatures that share that slot. Always persist the last signature you successfully processed.

Third, treating an empty page as proof that history ended. An empty page may only mean the requested window fell outside the node's retention. The correct interpretation is that an empty page is exhausted only when the walk began from null, meaning you started at the newest signature and walked backward to the true end of retained history.

  • Do not assume limit returns a full page.
  • Do not resume from a slot; resume from a signature.
  • Do not treat every empty page as the end of history.

A Gap-Free Walk Algorithm

A robust walker records the cursor as the last signature received, detects page-size shortfalls, verifies monotonic slot ordering inside the merged result, and flags an empty page as exhausted only when the walk began from null. This gives you a deterministic state machine rather than a hopeful loop.

Start with before unset and until unset. Request a page. If the page is empty and you started from null, mark exhausted and stop. If the page is empty and you did not start from null, mark a retention boundary and stop. If the page is non-empty, append entries, set before to the last signature, and continue.

After merging pages, assert that slots are monotonically non-increasing as you move from newest to oldest. A violation indicates a cursor bug or a provider-side inconsistency and should halt the walk rather than silently corrupt your index.

  • State: cursor (last signature), startedFromNull (boolean), exhausted (boolean).
  • On short page: record pageSizeReturned and continue unless empty.
  • On empty page: exhausted only if startedFromNull, else retention boundary.
  • Post-merge check: slots must be monotonic non-increasing.

Handling the Transaction-Retention Boundary

Provider nodes keep recent transaction data but may not serve very old transactions. A walk that crosses this boundary must distinguish 'this signature is older than the node can serve' from 'this signature does not exist'. The signature index and the transaction store can have different retention windows, so a listed signature does not guarantee a retrievable transaction.

The correct design is to persist the last successfully archived signature so that every future walk resumes from a known-good point. When getTransaction returns null for a signature you have already enumerated, record it as archived-unavailable rather than retrying indefinitely. This keeps the walk moving and preserves a clean resume point.

For long-term history, pagination alone cannot reconstruct data the cluster no longer retains. You need an archive strategy: persist transactions as you walk them, and treat the signature walk as a discovery mechanism rather than a retrieval guarantee. See Solana historical data over RPC for retention context.

  • Signature index retention and transaction retention can differ.
  • Persist last successfully archived signature as the resume point.
  • Null transaction: mark archived-unavailable, do not retry forever.

Runnable Node.js Walker with Reproducible Output

The following example walks an address with an explicit page size against an endpoint URL passed as an argument. It prints pageIndex, pageSizeReturned, firstSignature, lastSignature, firstSlot, lastSlot, and elapsedMs, and asserts that consecutive pages never overlap and never skip a slot. Run it against your own endpoint and address to build reproducible evidence.

Pass the RPC endpoint as the first argument and the address as the second. The script uses fetch, which is available in modern Node.js. Adjust PAGE_SIZE to a value well below your provider's maximum.

The assertions are intentionally strict: they will throw if a page overlaps the previous page or if slots are not monotonic. That is the point. You want the walker to fail loudly rather than silently produce a corrupt index.

// walk.mjs
// Usage: node walk.mjs <RPC_URL> <ADDRESS> [PAGE_SIZE]
const RPC_URL = process.argv[2];
const ADDRESS = process.argv[3];
const PAGE_SIZE = Number(process.argv[4] || 100);

if (!RPC_URL || !ADDRESS) {
  console.error('Usage: node walk.mjs <RPC_URL> <ADDRESS> [PAGE_SIZE]');
  process.exit(1);
}

async function rpc(method, params) {
  const res = await fetch(RPC_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.stringify(json.error));
  return json.result;
}

async function walk() {
  let before = null;
  let pageIndex = 0;
  let startedFromNull = true;
  let previousLastSignature = null;
  let previousFirstSlot = null;
  const seen = new Set();

  while (true) {
    const t0 = Date.now();
    const params = [ADDRESS, { limit: PAGE_SIZE }];
    if (before) params[1].before = before;
    const page = await rpc('getSignaturesForAddress', params);
    const elapsedMs = Date.now() - t0;

    const pageSizeReturned = page.length;
    const firstSignature = page[0]?.signature ?? null;
    const lastSignature = page[page.length - 1]?.signature ?? null;
    const firstSlot = page[0]?.slot ?? null;
    const lastSlot = page[page.length - 1]?.slot ?? null;

    console.log(JSON.stringify({
      pageIndex,
      pageSizeReturned,
      firstSignature,
      lastSignature,
      firstSlot,
      lastSlot,
      elapsedMs,
    }));

    if (pageSizeReturned === 0) {
      if (startedFromNull) console.log('exhausted: true');
      else console.log('retention boundary reached');
      break;
    }

    for (const entry of page) {
      if (seen.has(entry.signature)) {
        throw new Error('overlap detected: ' + entry.signature);
      }
      seen.add(entry.signature);
    }

    if (previousFirstSlot !== null && firstSlot > previousFirstSlot) {
      throw new Error('slot ordering violation: ' + firstSlot + ' > ' + previousFirstSlot);
    }

    previousLastSignature = lastSignature;
    previousFirstSlot = firstSlot;
    before = lastSignature;
    startedFromNull = false;
    pageIndex += 1;
  }
}

walk().catch((err) => {
  console.error('walk failed:', err.message);
  process.exit(1);
});

Measuring Your Own Endpoint: Results Table Guidance

Because maximum page size, retention, and rate limits are provider-and-cluster properties, you should measure them against your own endpoint rather than assume values. Run the walker above with a small page size and record the output. Then repeat with a larger page size and compare.

Fill in the table below with your own observations. The goal is to identify the page size at which short pages or timeouts begin to appear, and the point at which an empty page indicates retention rather than exhaustion.

Do not treat any single run as definitive. Provider behavior can change without notice, and the same endpoint may behave differently under load. Repeat the measurement at different times and record the variance.

  • Columns: pageSizeRequested, pageSizeReturned, elapsedMs, shortPage?, emptyPage?, notes.
  • Run at least three page sizes: small, medium, near suspected maximum.
  • Record whether the final empty page followed a full page or a short page.
| pageSizeRequested | pageSizeReturned | elapsedMs | shortPage? | emptyPage? | notes |
| --- | --- | --- | --- | --- | --- |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |

Commitment Levels and Provisional Pages

The commitment parameter interacts with the walk because confirmed and finalized histories diverge near the tip. A confirmed walk may include signatures that later disappear from the finalized history if a fork is resolved. An indexer should walk against the commitment level it actually needs and treat the most recent pages as provisional.

If you need finalized data, walk with commitment finalized and accept that the newest signatures may not yet be available. If you need low-latency data, walk with confirmed and be prepared to reconcile the tip later. Mixing commitment levels within a single walk produces inconsistent results.

For a deeper treatment of how commitment affects confirmation, see Solana commitment levels and transaction confirmation.

  • confirmed and finalized histories diverge near the tip.
  • Walk against the commitment level you actually need.
  • Treat the most recent pages as provisional and reconcile later.

WebSocket Transport and Cursor Safety

getSignaturesForAddress is a normal request/response call, not a subscription. Over a WebSocket transport, it behaves the same as over HTTP; the WebSocket path is about connection reuse rather than push semantics. There is no server-initiated stream of signatures for this method.

A naive resubscribe loop can restart a walk from the wrong cursor. If your WebSocket connection drops and you reconnect without preserving the last signature, you may begin again from the newest signature and duplicate work, or worse, resume from a stale in-memory cursor that no longer matches the server state.

Persist the cursor outside the connection lifecycle. Treat the WebSocket as a transport detail, not as a source of truth for walk state. For endpoint selection and transport guidance, see the RPC endpoints guide (RPC Assistant).

  • Not a subscription: no push semantics for this method.
  • Persist cursor outside the connection lifecycle.
  • On reconnect, resume from the last persisted signature.

Choosing a Page Size Below the Provider Maximum

The limit parameter should be chosen well below the provider's maximum. Smaller pages bound response size and failure blast radius, and retries become cheap. A full-page limit maximises the chance of a timeout that costs the entire page.

If a page times out, you lose the work for that page and must retry from the same cursor. With a smaller page, the retry is faster and the risk of repeated timeouts is lower. This is a tradeoff between request count and per-request reliability.

For timeout and retry patterns that complement this guidance, see Solana RPC timeouts and retries.

  • Smaller pages: lower blast radius, cheaper retries.
  • Full-page limit: higher timeout risk per request.
  • Tune page size against your own endpoint measurements.

Limitations, Tradeoffs, and Troubleshooting

Retention, maximum page size, and rate limits are provider-and-cluster properties that vary. Undocumented changes to them break long walks. A signature walk cannot reconstruct history that the cluster no longer retains, so long-term history requires an archive strategy rather than a pagination strategy.

Common failure modes include: a walk that stops early because until was reused across pages; a walk that duplicates signatures because it resumed from a slot; a walk that treats an empty page as exhaustion when it actually hit retention; and a walk that stalls because getTransaction returns null for an old signature.

To troubleshoot, log pageIndex, pageSizeReturned, firstSignature, lastSignature, firstSlot, lastSlot, and elapsedMs for every page. Compare consecutive pages for overlap and slot monotonicity. If a walk stops unexpectedly, check whether until was set and whether the last page was empty or short.

For migration of deprecated methods that may appear in older walkers, see Migrating deprecated Solana RPC methods. For network-specific endpoint details, see Solana networks.

  • Retention, page size, and rate limits vary by provider and cluster.
  • Pagination cannot recover data the cluster no longer retains.
  • Log per-page metrics to diagnose early stops and overlaps.
  • Distinguish retention boundary from true exhaustion.

Next Steps for Production Indexers

Move from a one-off walk to a durable indexer by persisting the last successfully archived signature, scheduling periodic walks, and reconciling the provisional tip against a finalized commitment level. Treat the signature walk as a discovery mechanism and the transaction fetch as the archival step.

Choose an endpoint that matches your retention and rate requirements. Review RPC pricing and the API service to understand provider-side constraints, and use the OnFinality Learn hub for adjacent guides on historical data, timeouts, and commitment.

Finally, validate your walker against your own address and endpoint using the results table guidance above. Reproducible evidence from your own measurements is the only reliable basis for tuning page size and retry policy.

  • Persist last archived signature as the resume point.
  • Schedule periodic walks and reconcile the tip.
  • Validate against your own endpoint before production.

Never Worry about Infrastructure Again

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

Get Started