Fix Solana RPC 429 Too Many Requests Errors
Why a Solana RPC returns 429 Too Many Requests, what web3.js does about it, and the fixes: rate limiting, Retry-After, backoff, fewer calls, streams.
Your bot logs Server responded with 429 Too Many Requests. Retrying after 500ms delay..., stalls for a few seconds, then fails with 429 Too Many Requests. The RPC is telling you that you sent more calls than it allows in some window of time.
Below: the limits, where the extra calls come from, what @solana/web3.js 1.x does about a 429, and the fixes, with a tested Node.js client.
Disclosure: AllenHark sells Solana RPC and Yellowstone gRPC. Other providers' docs are cited only for how they signal and count limits, checked November 2, 2026.
The short answer
| Do this | Why |
|---|---|
| Pace calls with a client-side rate limiter set below your limit | A burst trips limits your average rate never would |
On a 429, wait for Retry-After, then back off with jitter and a cap | Retrying at once, or all together, keeps you over |
Make fewer calls: cache, getMultipleAccounts, filtered getProgramAccounts | Every call counts, even inside a batch |
| Stream instead of polling | One subscription replaces a polling loop |
| Give each bot its own budget, or use a node with no rate limits | Processes on one key or IP share one limit |
What a 429 means on a Solana RPC
429 is the HTTP status Too Many Requests (RFC 6585): the client "has sent too many requests in a given amount of time". It may carry a Retry-After header with the wait, in seconds or as an HTTP date (RFC 9110). Each server decides how it counts: per IP, per key, per method or across servers.
So the limit belongs to whoever runs the endpoint. Some also put a JSON-RPC error in the body: Helius documents an HTTP 429 with {"code": -32005, "message": "Too many requests"}, and AllenHark RPC answers an address that opens more connections than its plan allows with HTTP 429 and error -32005. Check the HTTP status first; the body varies.
Two lookalikes don't go away by waiting: a 403 from Solana's public endpoints means your IP or website is blocked, and a 401 with -32001 from AllenHark RPC means a wrong, expired or revoked key.
Solana's public endpoint limits
Solana's docs list these limits for https://api.mainnet.solana.com, the public mainnet endpoint:
| Mainnet limit, per IP | Value |
|---|---|
| Requests per 10 seconds | 100 |
| Requests per 10 seconds for a single RPC | 40 |
| Concurrent connections | 40 |
| New connections per 10 seconds | 40 |
| Data per 30 seconds | 100 MB |
They add that the limits are "subject to change" and that the public endpoints "are not intended for production applications". For a 429 they say: "Slow down! Use the Retry-After HTTP response header to determine how long to wait before making another request."
That averages 10 calls a second in all, and 4 a second for a single method. A bot that reads five pools with getAccountInfo every 200 ms sends 25 a second.
Where the extra calls come from
- Polling loops. A slot is about 200 ms, so polling faster can't show new state more often, and the cost multiplies with every account you watch.
getProgramAccountsscans. Heavy, and some providers cap them on their own: Helius lists a separate per-second limit for it, and AllenHark allows 30 calls per 60 seconds per IP. On the public endpoint its bytes also count toward the 100 MB per 30 seconds.- Batches. One HTTP request, but QuickNode's guide is plain: "submitting them as a batch does not reduce the total request count." Plan on a batch of 50 costing 50.
- Retries without backoff. Retrying at once, or all together, adds calls exactly when you are over.
- Too many connections. In our test, 48 calls fired at once with plain
fetchopened 48 sockets; the public endpoint allows 40 concurrent connections per IP. web3.js 1.x's Node HTTP agent allows up to 25 sockets per host. A socket per WebSocket subscription, or reconnects in a tight loop, hit the same limits. - Shared keys and IPs. Five bots on one key or IP share one budget; each can stay under while the sum is over.
- Per-method limits. You can be under the total and still over for one method.
What web3.js 1.x does on a 429
Connection in @solana/web3.js 1.99.0, the latest 1.x release, retries a 429 by itself. The loop from src/connection.ts, trimmed:
let too_many_requests_retries = 5;
let waitTime = 500;
for (;;) {
res = await fetch(url, options);
if (res.status !== 429 /* Too many requests */) break;
if (disableRetryOnRateLimit === true) break;
too_many_requests_retries -= 1;
if (too_many_requests_retries === 0) break;
console.error(`Server responded with ${res.status} ${res.statusText}. Retrying after ${waitTime}ms delay...`);
await sleep(waitTime);
waitTime *= 2;
}- Five attempts in all, with waits of 500 ms, 1 s, 2 s and 4 s. After 7.5 seconds the call fails with
429 Too Many Requests:and the response body. Retry-Afteris ignored, and the waits have no jitter.- Each call retries on its own timer. Nothing slows the other calls, and calls that failed together retry together.
disableRetryOnRateLimit: truein theConnectionconfig turns the loop off; the call fails at once with the same error.
In our test (details below), 20 getSlot calls fired at once through a default Connection became 72 requests, 56 of them refused, arriving in waves at 0, 0.5, 1.5, 3.5 and 7.5 seconds. 4 calls still failed, and the console printed 52 "Retrying after" lines.
That is a retry storm: retries land while the server is refusing, too short and too regular to let it recover. Pace calls so the server doesn't have to refuse them, and make every caller back off together when it does.
Fix 1: pace your calls with a token bucket
A token bucket holds up to burst tokens and refills at rps tokens a second. Each call takes a token, a batch one per call inside it, and a call waits when the bucket is empty.
The sizing rule: in any window of W seconds, a bucket lets through at most burst + rps × W calls. Keep that below the limit for that window.
| Limit | Settings | Most calls in the window |
|---|---|---|
| Public mainnet, all methods: 100 per 10 s | rps: 9, burst: 5 | 5 + 90 = 95 |
| Public mainnet, one method: 40 per 10 s | rps: 3.5, burst: 3 | 3 + 35 = 38 |
| AllenHark RPC Standard: 100 requests per second | rps: 80, burst: 10 | 10 + 80 = 90 |
AllenHark getProgramAccounts or getSignaturesForAddress: 30 per 60 s per IP | rps: 0.4, burst: 5 | 5 + 24 = 29 |
Use one limiter per endpoint per process, shared by everything in it. If processes share a key or an IP, split the budget between them.
Fix 2: when a 429 comes anyway, back off properly
- Honor
Retry-After. It's the server telling you when it will take calls again. - Without it, use exponential backoff with full jitter: wait a random time between zero and
min(cap, base × 2^attempt). AWS's write-up on backoff and jitter shows why: backoff alone still leaves "clusters of calls", and jitter spreads them out. - Pause every caller, not only the one that got the 429. The limit is shared, so the wait should be too.
- Count retries against your budget, and cap the attempts. A trading bot has little use for a read that arrives ten seconds late.
- Don't retry 401 or 403. Fix the key or the address.
The code: a rate-limited JSON-RPC client
rpc-client.mjs needs only Node 18 or later. rateLimitedFetch returns a fetch that waits for tokens, caps requests in flight, and on a 429 or 503 pauses every caller for Retry-After plus jitter, or a fully jittered backoff, then retries up to maxAttempts. createRpc is a small JSON-RPC client on top.
// rpc-client.mjs: Solana JSON-RPC calls that stay under a rate limit.
// - A token bucket paces requests (plus optional buckets for single methods).
// - A cap on requests in flight (see the note on sockets below the code).
// - On a 429 every request pauses, for Retry-After if the server sent it,
// otherwise exponential backoff with full jitter, then retries a few times.
// Node 18 or later (global fetch). No dependencies.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export class TokenBucket {
constructor(perSecond, burst = perSecond) {
this.rate = perSecond / 1000; // tokens per millisecond
this.burst = burst;
this.tokens = burst;
this.last = performance.now();
this.pausedUntil = 0;
this.queue = Promise.resolve(); // first come, first served
}
take(n = 1) {
if (n > this.burst) return Promise.reject(new RangeError(`${n} calls can never fit a burst of ${this.burst}`));
this.queue = this.queue.then(async () => {
for (;;) {
const now = performance.now();
if (now < this.pausedUntil) { await sleep(this.pausedUntil - now); continue; }
this.tokens = Math.min(this.burst, this.tokens + Math.max(0, now - this.last) * this.rate);
this.last = Math.max(this.last, now);
if (this.tokens >= n) { this.tokens -= n; return; }
await sleep((n - this.tokens) / this.rate);
}
});
return this.queue;
}
// After a 429: nothing is sent for `ms`, and the bucket refills from empty.
pause(ms) {
const until = performance.now() + ms;
if (until > this.pausedUntil) Object.assign(this, { pausedUntil: until, last: until, tokens: 0 });
}
}
// Retry-After is a number of seconds or an HTTP date. Returns milliseconds, or null.
export function retryAfterMs(value, now = Date.now()) {
if (!value) return null;
if (/^\d+$/.test(value.trim())) return Number(value) * 1000;
const at = Date.parse(value);
return Number.isNaN(at) ? null : Math.max(0, at - now);
}
// Method names in a JSON-RPC body: one for a single call, several for a batch.
function methodsIn(body) {
try {
const parsed = JSON.parse(body);
return (Array.isArray(parsed) ? parsed : [parsed]).map((call) => call.method);
} catch {
return ["?"];
}
}
export function rateLimitedFetch({
rps, // requests per second for everything: stay under your plan
burst = 1, // calls allowed back to back; see the sizing rule
methods = {}, // per-method limits, e.g. { getProgramAccounts: { rps: 0.4, burst: 5 } }
maxInFlight = 8, // requests open at once
maxAttempts = 5, // the first try plus four retries
baseDelayMs = 250, // first backoff when there is no Retry-After
maxDelayMs = 10_000,
} = {}) {
if (!(rps > 0)) throw new Error("set rps below your provider's limit");
const all = new TokenBucket(rps, burst);
const perMethod = Object.fromEntries(
Object.entries(methods).map(([name, l]) => [name, new TokenBucket(l.rps, l.burst ?? 1)]),
);
const buckets = [all, ...Object.values(perMethod)];
let inFlight = 0;
const waiting = [];
const acquire = () => (inFlight < maxInFlight ? (inFlight++, undefined) : new Promise((r) => waiting.push(r)));
const release = () => (waiting.length ? waiting.shift()() : inFlight--);
return async function limitedFetch(url, init = {}) {
const calls = methodsIn(init.body);
for (let attempt = 1; ; attempt++) {
// A retry is a request too, so it waits for tokens like any other.
for (const name of new Set(calls)) {
if (perMethod[name]) await perMethod[name].take(calls.filter((m) => m === name).length);
}
await all.take(calls.length); // a batch of 5 calls costs 5
await acquire();
let res, body;
try {
res = await fetch(url, init);
body = await res.arrayBuffer(); // read it all, so the connection is free when the slot is
} finally {
release();
}
const reply = () => new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
if (res.status !== 429 && res.status !== 503) return reply();
const told = retryAfterMs(res.headers.get("retry-after"));
// Out of attempts, or told to wait longer than we will: hand the 429 back.
if (attempt >= maxAttempts || (told !== null && told > maxDelayMs)) return reply();
const wait = told !== null
? told + Math.random() * baseDelayMs // as told, plus a little jitter
: Math.random() * Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1)); // full jitter
for (const bucket of buckets) bucket.pause(wait); // one 429 slows every caller
console.warn(`HTTP ${res.status} (attempt ${attempt} of ${maxAttempts}); pausing ${Math.round(wait)} ms`);
}
};
}
// A minimal JSON-RPC client. Give every client for one endpoint the same
// limitedFetch, so they share one budget.
export function createRpc(url, limitedFetch, headers = {}) {
let id = 0;
async function post(body) {
const res = await limitedFetch(url, {
method: "POST",
headers: { "content-type": "application/json", ...headers },
body: JSON.stringify(body),
});
const text = await res.text();
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}: ${text.slice(0, 200)}`);
return JSON.parse(text);
}
return {
async call(method, params = []) {
const reply = await post({ jsonrpc: "2.0", id: ++id, method, params });
if (reply.error) throw new Error(`${method}: ${reply.error.code} ${reply.error.message}`);
return reply.result;
},
// One HTTP request, but each call in it counts toward the limit.
// Replies can come back in any order, so match them by id.
async batch(calls) {
const requests = calls.map(([method, params = []]) => ({ jsonrpc: "2.0", id: ++id, method, params }));
const byId = new Map((await post(requests)).map((reply) => [reply.id, reply]));
return requests.map((r) => byId.get(r.id)?.result ?? new Error(JSON.stringify(byId.get(r.id)?.error)));
},
};
}Using it, with the RPC URL from an environment variable. One limited function drives the client and a web3.js Connection, so they share one budget:
// example.mjs: RPC_URL=https://your-endpoint node example.mjs
// npm i @solana/web3.js@1 (only for the Connection part)
import { Connection } from "@solana/web3.js";
import { createRpc, rateLimitedFetch } from "./rpc-client.mjs";
const RPC_URL = process.env.RPC_URL;
if (!RPC_URL) throw new Error("Set RPC_URL");
// Sized for Solana's public mainnet endpoint: 100 calls per 10 s per IP in all
// (5 + 9 × 10 = 95) and 40 per 10 s for a single method (3 + 3.5 × 10 = 38).
const oneMethod = { rps: 3.5, burst: 3 };
const limited = rateLimitedFetch({
rps: 9,
burst: 5,
methods: { getSlot: oneMethod, getMultipleAccounts: oneMethod, getProgramAccounts: oneMethod },
});
// Everything below shares `limited`, so it shares one budget.
const rpc = createRpc(RPC_URL, limited);
const connection = new Connection(RPC_URL, {
commitment: "confirmed",
disableRetryOnRateLimit: true, // no built-in 500 ms, 1 s, 2 s, 4 s retries
fetch: limited, // ours instead
});
// Up to 100 accounts per getMultipleAccounts call, in place of one getAccountInfo each.
async function getAccounts(pubkeys) {
const pages = [];
for (let i = 0; i < pubkeys.length; i += 100) {
pages.push(rpc.call("getMultipleAccounts", [pubkeys.slice(i, i + 100), { encoding: "base64" }]));
}
return (await Promise.all(pages)).flatMap((page) => page.value);
}
// Token accounts of one mint: filtered on the server, and only bytes 32 to 71
// (owner and amount) of each 165-byte account come back.
const holders = (mint) =>
rpc.call("getProgramAccounts", [
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
{
encoding: "base64",
filters: [{ dataSize: 165 }, { memcmp: { offset: 0, bytes: mint } }],
dataSlice: { offset: 32, length: 40 },
},
]);
console.log("slot", await connection.getSlot());
const accounts = await getAccounts([
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"11111111111111111111111111111111",
]);
console.log("accounts", accounts.map((a) => (a ? `${a.owner} ${a.lamports}` : null)));
if (process.env.MINT) console.log("token accounts", (await holders(process.env.MINT)).length);- Sockets.
maxInFlightcaps requests, not sockets: in our test, 4 requests in flight through Node's built-infetchopened up to 7 sockets. If your provider limits connections, cap the pool with anundiciAgent: createnew Agent({ connections: 4 })and pass(url, init) => limited(url, { ...init, dispatcher })wherever you passedlimited. The same test then never opened more than 4. - Batches must fit the burst. The client refuses a batch bigger than
burstrather than wait forever.
How we tested it: against a local mock JSON-RPC server with the public endpoint's limits scaled to one second (10 calls a second, 4 for any one method, each call in a batch counted), answering anything over with HTTP 429, Retry-After: 1 and a -32005 body. Node 24.10.0, @solana/web3.js 1.99.0:
| Run | Requests sent | 429s | Calls failed | Most calls in any 1 s |
|---|---|---|---|---|
Plain fetch, 48 calls at once | 48 | 38 | 38 | 10 |
web3.js defaults, 20 getSlot at once | 72 | 56 | 4 | 4 |
rpc-client.mjs sized under both limits, 48 calls and a batch of 4 | 49 | 0 | 0 | 10 in all, 3 per method |
rpc-client.mjs sized too high (rps: 20), 48 calls | 61 | 13 | 0 | 10 |
web3.js with disableRetryOnRateLimit and rateLimitedFetch, 20 getSlot | 20 | 0 | 0 | 3 |
Sized too high, each 429 paused every caller until the next accepted request 1.1 to 1.3 seconds later, and all 48 calls succeeded. Against a server refusing everything, the client returned the 429 after five attempts, about 5 seconds. We also ran example.mjs once against the public mainnet endpoint: two read-only calls.
Fix 3: make fewer calls
- Cache. A blockhash lasts about 150 blocks, about 30 seconds: refresh it on a timer every second or two and read it from memory. Keep mint decimals, pool configs and lookup tables until they change.
- Read many accounts in one call.
getMultipleAccountstakes up to 100 public keys per call. The example splits longer lists into pages of 100. - Narrow
getProgramAccounts.filters(up to 4:memcmp,dataSize,tokenAccountState) return only matching accounts, anddataSliceonly the bytes you need. Token accounts are 165 bytes with the mint at byte 0 and the owner at byte 32, so the example'smemcmpon the mint plus a 40-byte slice returns each holder and amount. Scan once at startup, then keep current with a stream. - Batch for round trips, not quota.
Fix 4: stream instead of polling
- RPC WebSocket.
accountSubscribefor accounts you poll,programSubscribefor a program's accounts,logsSubscribefor transactions that mention one address, andsignatureSubscribeandslotSubscribein place of pollinggetSignatureStatusesandgetSlot. The default commitment isfinalized, so set yours. In web3.js (onAccountChange,onLogsand so on) they share one socket perConnection. Use few sockets, and reconnect with backoff: new connections count too. - Yellowstone gRPC. One
Subscribestream carries accounts, transactions, slots and blocks for all your filters. See our guides to stream lag and missed events and reconnecting withfrom_slot, and detecting new Pump.fun tokens and pools, which compares polling, WebSocket and gRPC for one real job.
Fix 5: spread the load, or remove the limit
- One budget per bot. If you have several keys or allowed addresses, give each bot its own, so one busy process can't starve the rest.
- Move to a node with no rate limits when your steady load sits near the cap. AllenHark's Dedicated RPC has no rate limits.
Symptom, cause and fix
| Symptom | Likely cause | Fix |
|---|---|---|
Server responded with 429 Too Many Requests lines, repeated | web3.js 1.x retrying on its own while over the limit | A shared limiter, plus disableRetryOnRateLimit: true |
| 429s at startup, fine afterwards | A burst: every account loaded at once | Smaller burst; getMultipleAccounts |
429s only on getProgramAccounts or getSignaturesForAddress | A per-method limit | A bucket for that method; filters and dataSlice; a stream |
| 429s in waves every few seconds | Callers retrying together | Jitter, and one pause for all callers |
| 429s since you added a second bot | Two processes on one key or IP | Split the budget, separate keys, or a dedicated node |
| 429s at a low call rate, many calls in parallel | A connection limit | Cap requests in flight and sockets |
| HTTP 403 from the public endpoint | Your IP or site is blocked | A private RPC endpoint |
HTTP 401 with -32001 from AllenHark RPC | A bad key, or an address not on your allowlist | Fix the key or the allowlist |
AllenHark RPC and gRPC
AllenHark RPC serves JSON-RPC and WebSocket on one host and port. From the product page and the RPC docs:
- Rate: 100 requests per second on the standard plan.
getProgramAccountsandgetSignaturesForAddressare limited to 30 calls per 60 seconds per IP, and agetProgramAccountscall with neitherfiltersnordataSliceis refused with-32602. - Connections: you buy a number of RPC Standard connections, on one IP or split across several. Each open socket uses one, and an IP that opens more than its share gets HTTP 429 with
-32005, so cap your socket pool to match. - Authentication: an IP on your allowlist, or an API key starting
ahk_, sent as thex-api-keyheader or as the first segment of the URL path; your access mode sets which are accepted. Pass the header as the third argument tocreateRpc, or in web3.js ashttpHeaders. - WebSocket: same port as HTTP, so set
wsEndpointin web3.js, which otherwise dials the next port up. - Dedicated RPC: no rate limits.
To stream instead of poll, AllenHark Yellowstone gRPC authenticates by allowlisted IP or, on new subscriptions, an API key in the x-token header, and replays about 48 hours with from_slot. On gRPC Standard each connection is one Subscribe stream, and an address that opens more than its share gets RESOURCE_EXHAUSTED, gRPC's version of a 429. Dedicated gRPC has no rate limits. See the gRPC docs and the pricing page.
FAQ
Why does my Solana RPC return 429 Too Many Requests?
You sent more calls, connections or data than the endpoint allows in some window. Common causes: polling loops, startup bursts, getProgramAccounts scans, retries without backoff and bots sharing one key or IP.
How do I stop web3.js printing "Server responded with 429 Too Many Requests"?
Set disableRetryOnRateLimit: true in the Connection config and pass a rate-limited fetch, like rateLimitedFetch above, as fetch.
Does a JSON-RPC batch get around the rate limit?
No. It saves round trips, but each call in it counts. Count a batch of 50 as 50 calls.
What is the public Solana mainnet RPC rate limit?
Per IP: 100 requests per 10 seconds, 40 per 10 seconds for a single RPC, 40 concurrent connections, 40 new connections per 10 seconds and 100 MB per 30 seconds, subject to change.
How many accounts can getMultipleAccounts fetch?
Up to 100 per call. Split longer lists into pages of 100 and send them through the same limiter.
Related reading
- Why your Yellowstone gRPC stream lags or misses events
- Reconnect Yellowstone gRPC without missing transactions
- Detect new Pump.fun tokens and pools in real time
- Alpenglow: what Solana RPC and gRPC users need to change
- Best Yellowstone gRPC providers in 2026
Sources
Checked November 2, 2026.
- Solana docs, Clusters and Public RPC Endpoints (source): mainnet limits, 403 and 429.
- Solana docs, getMultipleAccounts (source) and getProgramAccounts (source).
- Solana docs, WebSocket methods (logsSubscribe source).
@solana/web3.js1.99.0: connection.ts (retry loop, config, socket agent, one WebSocket perConnection) and makeWebsocketUrl.ts.- SPL Token, state.rs: token account layout.
- RFC 6585, section 4 and RFC 9110, section 10.2.3.
- JSON-RPC 2.0 specification: batch replies matched by
id. - AWS Architecture Blog, Exponential Backoff And Jitter.
- undici, Pool: the
connectionsoption. - QuickNode, guide to efficient RPC requests; Helius, rate limits.
- AllenHark: RPC docs, RPC, Yellowstone gRPC, gRPC docs.
