getProgramAccounts Without Melting Your App

Use Solana getProgramAccounts safely with filters, data slices, slot context, caching, and clear handoffs to streaming when raw RPC is the wrong tool.

getProgramAccounts Without Melting Your App

getProgramAccounts is useful, but it is one of the easiest Solana RPC methods to overuse.

The method asks an RPC node to return accounts owned by a program. That can be exactly what you need for a scoped lookup, or it can become an accidental full-program scan that burns latency, rate-limit budget, and backend memory.

Use this page when your app, bot, dashboard, or backend needs program-owned account data through Carbium RPC and you want a query policy that survives production traffic.

Part of the Carbium Solana infrastructure stack.


The short rule

Do not call getProgramAccounts like a database query.

Use it when all of these are true:

  • you know the program you are querying
  • you can filter by account size or byte offsets
  • the result set is bounded enough for a user-facing request
  • the data does not need to be recomputed on every render or every bot loop

If you need a continuous live view, use streaming or an indexed backend. If you need one wallet's token accounts, use a narrower RPC method such as getTokenAccountsByOwner.


What the method actually returns

Solana's official RPC documentation defines getProgramAccounts as returning accounts owned by a specified program, optionally filtered by account data or size. The request configuration can include:

OptionWhy it matters
commitmentChooses how finalized the returned account state must be
minContextSlotPrevents accepting a response evaluated before a known slot
withContextWraps the response with slot context
encodingControls how account data is returned
dataSliceReturns only part of each account's data
filtersNarrows the scan by account size or byte match

The operational problem is not that the method exists. The problem is calling it without enough constraints.


Start with a scoped request

This example uses the SPL Token Program and two filters:

  • dataSize: 165 for classic token-account layout size
  • memcmp offset 0 to match the mint field
curl https://rpc.carbium.io/?apiKey=$CARBIUM_RPC_KEY \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      {
        "commitment": "confirmed",
        "encoding": "base64",
        "withContext": true,
        "filters": [
          { "dataSize": 165 },
          {
            "memcmp": {
              "offset": 0,
              "bytes": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
            }
          }
        ]
      }
    ]
  }'

That query is still potentially large because USDC has many token accounts. In a real app, add the narrowest filter your use case supports, cache the result, or move the lookup out of the hot path.


Use dataSlice when you only need keys

If your first step only needs matching pubkeys, do not pull full account data.

curl https://rpc.carbium.io/?apiKey=$CARBIUM_RPC_KEY \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      {
        "commitment": "confirmed",
        "encoding": "base64",
        "withContext": true,
        "dataSlice": {
          "offset": 0,
          "length": 0
        },
        "filters": [
          { "dataSize": 165 },
          {
            "memcmp": {
              "offset": 32,
              "bytes": "5wx11hXBHQALycTQNkeQ5w1N9vgup4ardN2yLiDK4JyK"
            }
          }
        ]
      }
    ]
  }'

This pattern is useful for a two-step flow:

  1. find the matching account addresses
  2. fetch the specific accounts you need with a narrower follow-up read

It does not make an unbounded query safe. It only reduces response payload size after the node has evaluated the request.


Query policy for production apps

PatternRiskBetter policy
Calling getProgramAccounts on every page renderRepeated heavy scans and slow UICache by query key and refresh on a timer
Querying a busy program with no filtersHuge response or provider-side rejectionRequire dataSize and at least one memcmp when possible
Treating the result as fresh foreverStale app decisionsStore response slot and refresh policy
Using it for every bot loopRate-limit pressure and lagMove to gRPC, WebSocket, or an indexer path
Returning full account data when you only need pubkeysUnnecessary bandwidth and JSON parsingUse dataSlice or a narrower method
Retrying immediately after a timeoutRetry storms under loadBack off and log query shape before retrying

For rate-limit behavior and 429 handling, use Solana RPC Rate Limits Explained. This page focuses on designing the query so the limit is less likely to become the first problem.


A safer TypeScript wrapper

This wrapper forces every call to carry filters, context, and a timeout. Adapt the filter offsets to the account layout you are querying.

type MemcmpFilter = {
  memcmp: {
    offset: number;
    bytes: string;
  };
};

type ProgramAccountQuery = {
  programId: string;
  dataSize?: number;
  memcmp: MemcmpFilter[];
  dataSlice?: {
    offset: number;
    length: number;
  };
};

const RPC_URL = `https://rpc.carbium.io/?apiKey=${process.env.CARBIUM_RPC_KEY}`;

async function getScopedProgramAccounts(query: ProgramAccountQuery) {
  if (!query.dataSize && query.memcmp.length === 0) {
    throw new Error("Refuse unfiltered getProgramAccounts query");
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 8_000);

  try {
    const filters = [
      ...(query.dataSize ? [{ dataSize: query.dataSize }] : []),
      ...query.memcmp,
    ];

    const response = await fetch(RPC_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      signal: controller.signal,
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "getProgramAccounts",
        params: [
          query.programId,
          {
            commitment: "confirmed",
            encoding: "base64",
            withContext: true,
            ...(query.dataSlice ? { dataSlice: query.dataSlice } : {}),
            filters,
          },
        ],
      }),
    });

    const body = await response.json();

    if (body.error) {
      throw new Error(`${body.error.code}: ${body.error.message}`);
    }

    return body.result;
  } finally {
    clearTimeout(timeout);
  }
}

The wrapper is intentionally opinionated:

  • no empty scans
  • withContext by default
  • timeout instead of hanging workers
  • one place to add logging for method, filters, result count, slot, and duration

When to stop using raw RPC for this job

getProgramAccounts is a read tool, not a general-purpose query engine.

Move away from hot getProgramAccounts calls when:

  • users expect live updates every few seconds
  • the same expensive query runs across many workers
  • your app needs historical joins, sorting, or pagination
  • timeout/retry behavior becomes normal instead of exceptional
  • a missed update would be worse than a slower initial load

At that point, choose a clearer architecture:

NeedBetter fit
Live account or transaction changesCarbium gRPC
User wallet token accountsNarrow wallet/account RPC methods
Token metadata and logosCarbium Data API Calls
Historical or sortable app viewsYour own indexed store
Backup-path resilienceSafe RPC Failover Checklist

The right question is not "can RPC answer this once?" The right question is "should every user action ask RPC to recompute this again?"


Launch checklist

Before shipping a getProgramAccounts path:

  • You know the exact program ID and account layout.
  • You use dataSize, memcmp, or another intentional constraint.
  • You know the expected result count in normal traffic.
  • You store the response slot when freshness matters.
  • You cache or debounce user-driven reads.
  • You log method duration, result count, and error shape.
  • You have a fallback plan for large-result or timeout cases.
  • You have decided when the workload graduates to gRPC or an indexed backend.
🔶

Use Carbium RPC for scoped Solana reads, not accidental full-program scans. Start with a filtered query, measure the result, and move hot live workloads to the right Carbium surface when the query stops being a simple read.