getProgramAccounts returns every account owned by a program. On a popular program that can be hundreds of thousands of accounts, and the RPC has to scan all of them to answer.
★It is the single most expensive call in the standard JSON-RPC surface, and the one most likely to get you rate limited.★
What It Actually Does
There is no index behind it. The node walks the accounts owned by that program and applies your filters as it goes.
// ★Do not do this on a popular program.★
const all = await connection.getProgramAccounts(PROGRAM_ID);
That call transfers every account's full data. On a busy DEX program it is tens of megabytes, takes seconds, and counts heavily against whatever quota you have.
The response cost is your problem twice — once as provider quota, once as parsing time in your own process.
Filters Run Server-Side
Two filters do the real work, and they compose:
const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
filters: [
{ dataSize: 165 }, // ★only accounts of this exact size★
{
memcmp: {
offset: 32, // ★byte offset into account data★
bytes: owner.toBase58(), // base58-encoded value to match
},
},
],
});
dataSize matches accounts whose data is exactly that many bytes. Different account types in one program almost always have different sizes, so this alone often removes most of the set.
memcmp matches raw bytes at a fixed offset. ★This is how you find "all token accounts owned by this wallet" — offset 32 in a token account is the owner field.★
★Both filters are evaluated on the provider's side, so they reduce transfer, your parsing cost, and usually your billed usage at the same time.★
If your reads are tuned and the problem is transactions landing, a free BoltTx key is one line to test the submission path.
dataSlice: Cut the Payload, Not the Match Set
If you only need a few bytes from each account, ask for only those bytes:
const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
filters: [{ dataSize: 165 }],
dataSlice: { offset: 64, length: 8 }, // ★just the amount field★
});
★Matching a thousand accounts and transferring their full data when you need eight bytes each is a hundredfold waste on the wire.★ dataSlice cuts the response without changing which accounts match.
The combination that keeps this call affordable is all three together: dataSize to narrow the type, memcmp to narrow the set, dataSlice to narrow the payload.
Getting the Offsets Right
A memcmp offset is a byte position in the account's serialized layout, and getting it wrong returns an empty result rather than an error.
For an SPL token account:
offset 0 mint 32 bytes
offset 32 ★owner★ 32 bytes
offset 64 amount 8 bytes
offset 72 delegate 36 bytes
...
★total 165 bytes★
For an Anchor account, ★the first 8 bytes are the discriminator★, so every field offset shifts by 8 relative to the struct definition. Forgetting that is the most common reason a filter silently matches nothing.
// ★Anchor: 8-byte discriminator first.★
{ memcmp: { offset: 8, bytes: authority.toBase58() } }
Verify against a known account before trusting a filter. Fetch one account you know should match, decode it, and confirm the value is where you think it is.
When Not to Use It At All
★getProgramAccounts answers "what exists right now." It is the wrong tool for three common jobs.★
Watching for changes. Polling it on a timer is expensive and slow. programSubscribe pushes updates as they happen, with the same filter options.
Historical queries. It only sees current state. Anything time-based needs your own storage.
High-frequency lookups. If you need the same set repeatedly, fetch once and maintain it from a subscription rather than re-scanning.
A pattern that works well for bots:
// ★One expensive scan for the snapshot.★
const initial = await connection.getProgramAccounts(PROGRAM_ID, { filters });
// ★Then cheap incremental updates.★
connection.onProgramAccountChange(
PROGRAM_ID,
(info) => updateCache(info),
"processed",
filters,
);
★Snapshot once, then stream.★ This turns a repeated heavy call into a single one plus a subscription, which is usually the difference between fitting inside a rate limit and not.
When It Fails
| Symptom | Cause |
|---|---|
| 429 | ★Too many scans, or unfiltered★ |
| Timeout | Result set too large |
| Empty result | ★Wrong offset, often the Anchor discriminator★ |
| Wrong accounts | dataSize matches another type too |
| Slow but working | Missing dataSlice |
★Remember the empty-result row.★ A wrong offset and a genuinely empty set both return [], so it reads as "no accounts exist" when it usually means "your offset is wrong." Both a bad offset and a genuinely empty set return [].
Some providers disable or restrict this method on shared tiers precisely because of its cost. If it works on one endpoint and not another, that is usually policy rather than a bug.
What Landing Looks Like
Real transactions through our delivery nodes: median confirmation 336ms — under one slot.
★Reading and sending are separate paths with separate limits.★ A bot throttled on getProgramAccounts will also fail to send, not because sending is rate limited, but because it shares the quota the scans exhausted.
Where BoltTx Fits
We handle submission, not indexing. Keep whatever you use for reads.
Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a transaction is not observable in transit before it lands. ★Because the endpoints are independent, heavy read traffic cannot exhaust the path your transactions go out on.★
You sign locally. We never hold funds, never sign, and never modify transaction contents. The tip travels inside the transaction, paid on chain from your own wallet, and reverts with the transaction if it fails, because that is how Solana handles atomic transactions. You pay only on transactions that reach the chain.
Get a free API key. No monthly fee:
// Reads stay where they are; only the send endpoint changes.
const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
FAQ
What does getProgramAccounts do? It returns every account owned by a program, optionally filtered. There is no index behind it, so the node scans the program's accounts to answer, which is why it is expensive.
Why is getProgramAccounts so slow? Because it scans rather than looks up. On a popular program that is hundreds of thousands of accounts, and without filters it also transfers all their data.
How do memcmp filters work?
They match raw bytes at a fixed offset in the account data. Offset 32 in a token account is the owner, so a memcmp there finds every token account belonging to a wallet.
Why does my memcmp filter return nothing? Usually a wrong offset. For Anchor accounts the first 8 bytes are a discriminator, so every field shifts by 8 from the struct definition. A bad offset returns an empty array, not an error.
What is dataSize used for? Matching accounts of exactly that byte length. Different account types in one program usually have different sizes, so it is often the cheapest way to exclude most of the set.
How do I reduce the response size?
Use dataSlice to request only the bytes you need. Matching a thousand accounts and transferring full data when you need eight bytes each is a large and avoidable waste.
Why am I getting rate limited on getProgramAccounts?
Because each call is heavy and providers price it accordingly. Filter server-side, add dataSlice, and replace repeated scans with one snapshot plus a subscription.
Is getProgramAccounts good for monitoring changes?
No. It answers what exists now. Use programSubscribe with the same filters to receive updates as they happen instead of re-scanning on a timer.
Can I query historical state with getProgramAccounts? No, it only sees current state. Anything time-based requires storing snapshots yourself, since the method has no notion of a past slot.
Why does it work on one RPC provider but not another? Many providers restrict or disable it on shared tiers because of its cost. If the same call succeeds elsewhere, that is usually a policy difference rather than a bug.
What is the Anchor discriminator offset? Eight bytes at the start of every Anchor account, identifying the account type. Filters against Anchor data must add 8 to the offset you would compute from the struct alone.
How do I find the right offset for a filter? From the account layout — either the program's IDL or its documented struct. Verify by fetching one known account and confirming the value sits where you expect before trusting the filter.
Can I use several filters at once?
Yes, and you generally should. dataSize plus memcmp compose, and each additional filter reduces both the scan result and the data transferred.
What is the best pattern for a bot that needs current state?
Snapshot once with a filtered getProgramAccounts, then keep it current with onProgramAccountChange. That replaces repeated heavy scans with one call plus a stream.
Does getProgramAccounts count against the same quota as sending? On a shared endpoint, usually yes, which is why heavy reads can starve your sends. Separating the read path from the send path avoids that interaction entirely.
How many accounts can getProgramAccounts return? There is no fixed cap, which is the problem — large result sets time out or get truncated by the provider. Filter enough that the result is bounded by design.