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.

  • November 3, 2026
  • Infrastructure
  • AllenHark Team
A quiet stack of pagesAn abstract drawing of light on the page's dark ground. A neat stack of thin, solid pages, the top one lit, with faint streams of light drifting toward it from the left.A quiet stack of pagesAn abstract drawing of light on the page's dark ground. A neat stack of thin, solid pages, the top one lit, with faint streams of light drifting toward it from the left.

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 thisWhy
Pace calls with a client-side rate limiter set below your limitA burst trips limits your average rate never would
On a 429, wait for Retry-After, then back off with jitter and a capRetrying at once, or all together, keeps you over
Make fewer calls: cache, getMultipleAccounts, filtered getProgramAccountsEvery call counts, even inside a batch
Stream instead of pollingOne subscription replaces a polling loop
Give each bot its own budget, or use a node with no rate limitsProcesses 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 IPValue
Requests per 10 seconds100
Requests per 10 seconds for a single RPC40
Concurrent connections40
New connections per 10 seconds40
Data per 30 seconds100 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.
  • getProgramAccounts scans. 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 fetch opened 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:

JavaScript
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-After is 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: true in the Connection config 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.

LimitSettingsMost calls in the window
Public mainnet, all methods: 100 per 10 srps: 9, burst: 55 + 90 = 95
Public mainnet, one method: 40 per 10 srps: 3.5, burst: 33 + 35 = 38
AllenHark RPC Standard: 100 requests per secondrps: 80, burst: 1010 + 80 = 90
AllenHark getProgramAccounts or getSignaturesForAddress: 30 per 60 s per IPrps: 0.4, burst: 55 + 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.

JavaScript
// 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:

JavaScript
// 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. maxInFlight caps requests, not sockets: in our test, 4 requests in flight through Node's built-in fetch opened up to 7 sockets. If your provider limits connections, cap the pool with an undici Agent: create new Agent({ connections: 4 }) and pass (url, init) => limited(url, { ...init, dispatcher }) wherever you passed limited. The same test then never opened more than 4.
  • Batches must fit the burst. The client refuses a batch bigger than burst rather 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:

RunRequests sent429sCalls failedMost calls in any 1 s
Plain fetch, 48 calls at once48383810
web3.js defaults, 20 getSlot at once725644
rpc-client.mjs sized under both limits, 48 calls and a batch of 4490010 in all, 3 per method
rpc-client.mjs sized too high (rps: 20), 48 calls6113010
web3.js with disableRetryOnRateLimit and rateLimitedFetch, 20 getSlot20003

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. getMultipleAccounts takes 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, and dataSlice only the bytes you need. Token accounts are 165 bytes with the mint at byte 0 and the owner at byte 32, so the example's memcmp on 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. accountSubscribe for accounts you poll, programSubscribe for a program's accounts, logsSubscribe for transactions that mention one address, and signatureSubscribe and slotSubscribe in place of polling getSignatureStatuses and getSlot. The default commitment is finalized, so set yours. In web3.js (onAccountChange, onLogs and so on) they share one socket per Connection. Use few sockets, and reconnect with backoff: new connections count too.
  • Yellowstone gRPC. One Subscribe stream carries accounts, transactions, slots and blocks for all your filters. See our guides to stream lag and missed events and reconnecting with from_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

SymptomLikely causeFix
Server responded with 429 Too Many Requests lines, repeatedweb3.js 1.x retrying on its own while over the limitA shared limiter, plus disableRetryOnRateLimit: true
429s at startup, fine afterwardsA burst: every account loaded at onceSmaller burst; getMultipleAccounts
429s only on getProgramAccounts or getSignaturesForAddressA per-method limitA bucket for that method; filters and dataSlice; a stream
429s in waves every few secondsCallers retrying togetherJitter, and one pause for all callers
429s since you added a second botTwo processes on one key or IPSplit the budget, separate keys, or a dedicated node
429s at a low call rate, many calls in parallelA connection limitCap requests in flight and sockets
HTTP 403 from the public endpointYour IP or site is blockedA private RPC endpoint
HTTP 401 with -32001 from AllenHark RPCA bad key, or an address not on your allowlistFix 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. getProgramAccounts and getSignaturesForAddress are limited to 30 calls per 60 seconds per IP, and a getProgramAccounts call with neither filters nor dataSlice is 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 the x-api-key header or as the first segment of the URL path; your access mode sets which are accepted. Pass the header as the third argument to createRpc, or in web3.js as httpHeaders.
  • WebSocket: same port as HTTP, so set wsEndpoint in 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.

Sources

Checked November 2, 2026.