Bittensor (Finney) RPC endpoints enforce per-IP rate limits, typically 1-5 requests per second on public nodes. This article explains how rate limiting works on Substrate JSON-RPC, what a 429 looks like, and how to avoid it using subscriptions, batching, caching, and backoff. It includes a runnable Node.js example and a results table for your own benchmarks.
Direct Answer: What Are Bittensor RPC Rate Limits?
Bittensor (Finney) RPC endpoints, like most public Substrate JSON-RPC services, enforce per-IP rate limits. On shared community nodes, the cap is typically around 1 request per second (RPS) per IP, while some public or authenticated providers scale to 5 RPS or higher. When you exceed the limit, the server responds with HTTP 429 (Too Many Requests) or a JSON-RPC error like Rate limit exceeded. This is not a timeout; it's an explicit signal that you are sending too many requests in a given window.
The exact limits vary by provider. For example, OnFinality's Bittensor Finney endpoint may have different caps than a community node. Independent performance comparisons, such as those from comparenodes, provide a snapshot of public endpoints, but you should always benchmark your own usage against your chosen provider. This article separates documented Bittensor behavior from provider-specific limits and gives you a reproducible method to measure your own endpoint's tolerance.
- Public Bittensor RPC endpoints typically allow 1-5 RPS per IP.
- 429 or
Rate limit exceededindicates you've hit the cap, not a network issue. - Heavy calls like
state_getMetadataandstate_callconsume more allowance. - Use subscriptions, batching, and caching to stay under the cap.
How Bittensor (Substrate) JSON-RPC Rate Limiting Works
Bittensor's Finney network is built on Substrate, so its JSON-RPC interface follows the Substrate specification. Rate limiting is typically implemented at the HTTP or WebSocket layer, often via a middleware that tracks requests per IP address. The server may use a token bucket or sliding window algorithm to enforce a maximum number of requests per second.
When a request exceeds the limit, the server returns an HTTP 429 status code (for HTTP) or a JSON-RPC error with code -32005 or a custom message like Rate limit exceeded (for WebSocket). This is different from a timeout, which occurs when the server is slow to respond but hasn't rejected the request. A 429 is a definitive rejection that you must handle with retry logic.
Some calls are more expensive than others. For example, state_getMetadata returns the entire runtime metadata, which can be large, and state_call executes a runtime function, which can be CPU-intensive. Providers may weight these calls more heavily or apply stricter limits to them. Similarly, polling every block with chain_getBlock or chain_getHeader can quickly exhaust your allowance, especially if you're also making other calls.
- Rate limiting is per-IP, not per-account or per-API key (unless you have an authenticated endpoint).
- HTTP and WebSocket endpoints may have separate limits.
- Expensive calls like
state_getMetadataandstate_callmay be throttled more aggressively. - Block polling is a common cause of 429s because it generates a request every few seconds.
What a 429 Looks Like on Substrate JSON-RPC
When you hit a rate limit, the response format depends on the transport. For HTTP, you'll get an HTTP 429 status code with a body that may contain a JSON-RPC error object. For WebSocket, the server may close the connection or send a JSON-RPC error message.
Here's an example of an HTTP 429 response from a Substrate node:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{"jsonrpc":"2.0","error":{"code":-32005,"message":"Rate limit exceeded: 1 req/s"},"id":1}Why Block Polling and Heavy State Calls Trigger Limits
Many Bittensor applications poll the chain for new blocks using chain_getBlock or chain_getHeader in a loop. If you poll every 6 seconds (the block time), that's 10 requests per minute, which is fine. But if you also make other calls like state_getMetadata or state_call for each block, you can easily exceed a 1 RPS cap.
For example, a monitoring bot that fetches the latest block, then calls state_call to read a specific storage value, then calls state_getMetadata to decode events, could make 3 requests per block. At 1 block every 6 seconds, that's 0.5 RPS, which is under 1 RPS. But if you're also polling multiple endpoints or making retries, you can spike above the cap.
The solution is to use chain_subscribeFinalizedHeads (WebSocket) to receive new block headers as they are finalized, eliminating the need for polling. This reduces the number of requests dramatically. For storage reads, use state_getStorage with specific keys instead of state_call when possible, and cache the results.
- Block polling generates a request every block, which adds up quickly.
state_getMetadatais a large response and may be rate-limited more strictly.state_callexecutes runtime code and is CPU-intensive.- Use
chain_subscribeFinalizedHeadsto get block updates via subscription instead of polling.
Staying Under the Cap: Subscriptions, Batching, and Caching
To avoid 429s, you need to reduce the number of requests you make. Here are the most effective strategies:
Use WebSocket subscriptions for new blocks and events. chain_subscribeFinalizedHeads gives you a stream of block headers without polling. Similarly, state_subscribeStorage can notify you when specific storage keys change.
Batch requests using JSON-RPC batch. Substrate supports sending an array of requests in a single HTTP POST. This counts as one request against the rate limit, even if it contains multiple calls. For example, you can batch several state_getStorage calls into one request.
Cache expensive reads like state_getMetadata. Metadata rarely changes, so fetch it once and cache it locally. For storage values that change infrequently, cache them with a TTL.
Use a dedicated endpoint or self-hosted subtensor node if you need more headroom. For mining or monitoring bots that require high throughput, a public endpoint's rate limit may be too restrictive. OnFinality's API service offers dedicated endpoints with higher limits, and you can also run your own node.
- Subscriptions reduce request count by pushing data to you instead of you polling.
- Batching multiple calls into one HTTP request counts as one request.
- Cache metadata and frequently-read storage values.
- For high-throughput needs, consider a dedicated endpoint or self-hosted node.
Reproducible Example: Node.js Request Loop with Backoff
The following Node.js script demonstrates how to make a request to a Bittensor RPC endpoint, handle a 429 response, and retry with exponential backoff. It also shows how to batch multiple calls into one request. You can run this against any public Bittensor RPC endpoint to observe its rate limit behavior.
Prerequisites: Node.js 18+ and the ws package (for WebSocket). Install with npm install ws.
Script:
const WebSocket = require('ws');
const RPC_URL = 'wss://your-bittensor-endpoint.example';
function subscribeToHeads() {
const ws = new WebSocket(RPC_URL);
ws.on('open', () => {
ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'chain_subscribeFinalizedHeads',
params: []
}));
});
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.method === 'chain_subscribeFinalizedHeads') {
console.log('New finalized head:', msg.params.result.number);
}
});
ws.on('error', (err) => {
console.error('WebSocket error:', err.message);
});
}
// Example of batching multiple state_getStorage calls into one HTTP request
async function batchGetStorage(keys) {
const batch = keys.map((key, i) => ({
jsonrpc: '2.0',
id: i + 1,
method: 'state_getStorage',
params: [key]
}));
const response = await fetch(RPC_URL.replace('wss', 'https'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(batch)
});
if (response.status === 429) {
console.log('Rate limited, retrying...');
await new Promise(resolve => setTimeout(resolve, 1000));
return batchGetStorage(keys);
}
return response.json();
}
// Run the subscription
subscribeToHeads();
// Example usage of batchGetStorage (uncomment to test)
// batchGetStorage(['0x...key1', '0x...key2']).then(console.log);Expected Output and Verification
When you run the script, you should see a stream of block numbers printed as new finalized heads arrive. If the endpoint enforces a rate limit, you may see WebSocket errors or disconnections if you send too many requests. The batch function will return an array of JSON-RPC responses, one per key.
To verify the rate limit, you can intentionally send a burst of requests and observe the 429 responses. The following table is for you to fill in with your own measurements. Run the script against different endpoints and record the results.
Results Table (fill in your own measurements):
- Endpoint URL: [your endpoint]
- Observed rate limit (RPS): [e.g., 1, 5]
- Response code on limit: [429 or JSON-RPC error]
- Retry after header: [e.g., 1 second]
- Notes: [e.g., WebSocket disconnects after 3 violations]
Common Failures and Fixes
Failure: 429 on every request. This usually means you're exceeding the limit consistently. Check your request rate and reduce it. Use a subscription instead of polling, and batch independent calls.
Failure: WebSocket connection drops. Some providers disconnect WebSocket clients that exceed the limit. Implement reconnection logic with exponential backoff. The ws library supports reconnect via the reconnect option or you can handle it manually.
Failure: state_getMetadata is slow or rate-limited. Cache the metadata locally and refresh it only when the runtime upgrades. You can detect runtime upgrades by subscribing to chain_subscribeRuntimeVersion.
Failure: state_call is expensive. Use state_getStorage with specific keys instead of state_call when possible. If you must use state_call, cache the results and avoid calling it in a loop.
- If you get 429s, reduce request frequency or use batching.
- Implement reconnection with backoff for WebSocket subscriptions.
- Cache metadata and runtime version to avoid repeated expensive calls.
- Prefer
state_getStorageoverstate_callfor simple storage reads.
Tradeoffs and Limitations
While subscriptions and batching reduce request count, they have tradeoffs. Subscriptions require a persistent WebSocket connection, which may not be suitable for serverless functions or short-lived processes. Batching increases latency for individual calls because you wait for all responses before processing.
Rate limits are per-IP, so if you're behind a shared IP (e.g., a corporate NAT), you may be affected by other users' traffic. Using an authenticated endpoint with an API key can give you a dedicated allowance. OnFinality's RPC pricing page explains the options.
Also, note that public endpoints may have different limits for different methods. For example, chain_getBlock might be limited to 1 RPS, while system_health might be 5 RPS. Always check the provider's documentation or test empirically.
- Subscriptions require a persistent connection; not ideal for all use cases.
- Batching adds latency; use it for non-time-sensitive calls.
- Per-IP limits can be affected by other users on the same IP.
- Authenticated endpoints may offer higher limits.
Next Steps and Further Reading
Now that you understand Bittensor RPC rate limits, you can optimize your application to stay under the caps. For more guidance, explore the following resources:
- Bittensor Finney network overview – learn about the network and its endpoints.
- OnFinality Learn hub – more tutorials and deep dives on RPC and blockchain infrastructure.
- RPC pricing – compare plans and dedicated endpoint options.
- API service – get dedicated endpoints with higher limits.
- Bittensor RPC guidance (RPC Assistant) – quick answers to common Bittensor RPC questions.
- RPC monitoring and failover – ensure high availability with monitoring and failover strategies.
Remember to benchmark your own endpoints and adjust your strategy based on your specific needs. Independent comparisons like comparenodes can give you a starting point, but your mileage may vary.