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:
| Option | Why it matters |
|---|---|
commitment | Chooses how finalized the returned account state must be |
minContextSlot | Prevents accepting a response evaluated before a known slot |
withContext | Wraps the response with slot context |
encoding | Controls how account data is returned |
dataSlice | Returns only part of each account's data |
filters | Narrows 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: 165for classic token-account layout sizememcmpoffset0to 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
dataSlice when you only need keysIf 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:
- find the matching account addresses
- 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
| Pattern | Risk | Better policy |
|---|---|---|
Calling getProgramAccounts on every page render | Repeated heavy scans and slow UI | Cache by query key and refresh on a timer |
| Querying a busy program with no filters | Huge response or provider-side rejection | Require dataSize and at least one memcmp when possible |
| Treating the result as fresh forever | Stale app decisions | Store response slot and refresh policy |
| Using it for every bot loop | Rate-limit pressure and lag | Move to gRPC, WebSocket, or an indexer path |
| Returning full account data when you only need pubkeys | Unnecessary bandwidth and JSON parsing | Use dataSlice or a narrower method |
| Retrying immediately after a timeout | Retry storms under load | Back 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
withContextby 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:
| Need | Better fit |
|---|---|
| Live account or transaction changes | Carbium gRPC |
| User wallet token accounts | Narrow wallet/account RPC methods |
| Token metadata and logos | Carbium Data API Calls |
| Historical or sortable app views | Your own indexed store |
| Backup-path resilience | Safe 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.
Updated 1 day ago
