Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides12 min read

Bittensor WebSocket RPC: Substrate Subscriptions and Reliable Streams

Learn how to use Bittensor's WebSocket RPC for real-time Substrate subscriptions: chain heads, storage, and extrinsics, with a Node.js example and troubleshooting.

TL;DR

This guide explains Bittensor's WebSocket RPC for Substrate-based subscriptions, covering the JSON-RPC format, connection lifecycle, and a runnable Node.js example using @polkadot/api, plus troubleshooting for common disconnections.

What Is Bittensor WebSocket RPC?

Bittensor's WebSocket RPC is the real-time interface to the Finney network, a Substrate (Polkadot SDK) blockchain. It allows you to subscribe to live updates such as new blocks, finalized heads, storage changes, and extrinsic statuses. Unlike HTTP RPC, WebSocket maintains a persistent connection, enabling the node to push notifications asynchronously. This guide shows you how to use it reliably, with a focus on the Substrate JSON-RPC subscription API.

The primary endpoint is wss://finney-rpc.onfinality.io (or your provider's URL). Public endpoints often have rate limits and connection caps, so understanding the lifecycle is key to building robust applications.

Substrate's RPC system is defined by the JSON-RPC 2.0 specification, and the subscription methods follow a consistent pattern: you send a request with a method name ending in _subscribe, receive a subscription ID, and then receive notifications with a corresponding _unsubscribe method to cancel. This pattern is documented in the Substrate RPC documentation.

For Bittensor specifically, the Finney network runs a Substrate-based chain with custom pallets for the incentive mechanism. The core RPC methods are standard Substrate, but you may also encounter custom methods for Bittensor-specific features. Always check the Bittensor documentation for the latest list of supported RPC methods.

  • Bittensor is built on Substrate, so it supports standard Substrate RPC methods.
  • WebSocket subscriptions are essential for miners, validators, and dApps that need real-time data.
  • This guide covers reading chain state, not mining/registration (which uses a dedicated Subtensor flow).

Substrate JSON-RPC Subscription API

The Substrate JSON-RPC API provides several subscription methods. The most common are: chain_subscribeNewHeads, chain_subscribeFinalizedHeads, state_subscribeStorage, and author_submitAndWatchExtrinsic. Each returns a subscription ID and sends notifications as JSON-RPC messages.

The request format is standard JSON-RPC 2.0: {"jsonrpc":"2.0","id":1,"method":"chain_subscribeNewHeads","params":[]}. The response includes a result with the subscription ID. Notifications arrive as {"jsonrpc":"2.0","method":"chain_newHead","params":{"subscription":"sub_id","result":{...}}}.

For example, a raw WebSocket request to subscribe to new heads would look like this:

{"jsonrpc":"2.0","id":1,"method":"chain_subscribeNewHeads","params":[]}
And the response might be:
{"jsonrpc":"2.0","result":"0x1234","id":1}
Then you receive notifications like:
{"jsonrpc":"2.0","method":"chain_newHead","params":{"subscription":"0x1234","result":{"number":"0x1a2b","hash":"0x...","parentHash":"0x...","stateRoot":"0x...","extrinsicsRoot":"0x...","digest":{...}}}}

The @polkadot/api library abstracts these raw messages, but understanding the underlying format helps with debugging and when using other clients.

  • chain_subscribeNewHeads – best block headers (may be reorged).
  • chain_subscribeFinalizedHeads – finalized block headers (safe for consensus).
  • state_subscribeStorage – storage changes for specific keys.
  • author_submitAndWatchExtrinsic – track extrinsic status (e.g., ready, inBlock, finalized).

Connection Lifecycle and Why Public WSS Endpoints Drop

Public Bittensor WSS endpoints are shared resources. They typically enforce idle timeouts (e.g., 60 seconds without messages), per-IP connection caps, and load balancing that may terminate connections. When a connection drops, you must reconnect and resubscribe to all active subscriptions.

The @polkadot/api library handles reconnection automatically if you use ApiPromise with the provider option. However, you must ensure your code resubscribes idempotently – meaning it can safely re-run the subscription logic without duplicating handlers.

The WebSocket protocol includes a built-in ping/pong mechanism, but not all clients implement it. To keep the connection alive, you can send a JSON-RPC request like {"jsonrpc":"2.0","method":"system_health","params":[],"id":1} periodically. This is a lightweight call that returns node health and resets the idle timer.

Load balancers may also close connections during maintenance or scaling events. Implementing exponential backoff with jitter is a best practice to avoid thundering herd problems. The Polkadot.js documentation provides guidance on reconnection strategies.

  • Idle timeouts: send a ping or keepalive message to prevent drops.
  • Connection caps: limit concurrent connections per IP; use a single connection for multiple subscriptions.
  • Load balancing: nodes may redirect or close connections; implement exponential backoff.

Runnable Node.js Example with @polkadot/api

Below is a complete Node.js script that connects to Bittensor's WebSocket RPC, subscribes to new heads and finalized heads, and logs the block numbers. It also demonstrates reconnection handling using on('connected') and on('disconnected') events.

To run it, install @polkadot/api and ws (if needed). The script uses ApiPromise.create with the WSS provider. It subscribes to subscribeNewHeads and subscribeFinalizedHeads, printing block numbers and hashes. Expected output shows a stream of block headers.

The script sets autoConnect to false to manually control the connection, which is useful for implementing custom reconnection logic. The provider.on('connected') and provider.on('disconnected') events allow you to log connection status and trigger resubscription if needed.

In a production environment, you would wrap the subscription logic in a function that can be called again on reconnect, ensuring that you don't create duplicate subscriptions. The unsub functions returned by the subscription calls can be used to clean up before reconnecting.

  • Use api.rpc.chain.subscribeNewHeads() to get best block headers.
  • Use api.rpc.chain.subscribeFinalizedHeads() for finalized headers.
  • Handle disconnected events to trigger reconnection logic.
// Install: npm install @polkadot/api
const { ApiPromise, WsProvider } = require('@polkadot/api');

const WS_URL = 'wss://finney-rpc.onfinality.io';

async function main() {
  const provider = new WsProvider(WS_URL, false); // autoConnect false for manual control
  const api = await ApiPromise.create({ provider });

  provider.on('connected', () => console.log('Connected'));
  provider.on('disconnected', () => console.log('Disconnected'));

  // Subscribe to new heads (best block)
  const unsubNew = await api.rpc.chain.subscribeNewHeads((header) => {
    console.log(`New head: #${header.number} hash=${header.hash}`);
  });

  // Subscribe to finalized heads
  const unsubFinal = await api.rpc.chain.subscribeFinalizedHeads((header) => {
    console.log(`Finalized: #${header.number} hash=${header.hash}`);
  });

  // Keep running; to stop, call unsubNew() and unsubFinal()
}

main().catch(console.error);

// Expected output (example):
// Connected
// New head: #123456 hash=0x...
// Finalized: #123455 hash=0x...
// ...

Subscribing to Storage Changes with Key Filtering

To monitor specific storage items, use state_subscribeStorage with a list of storage keys. You must provide the hashed key (twox_64 concat blake2_256) or use the @polkadot/api to generate it. For example, to watch a specific account's balance, you can use api.query.system.account and pass the key.

The notification includes the key and the new value. This is efficient because you only receive changes for the keys you care about, reducing bandwidth and processing.

The storage key generation follows Substrate's storage hashing scheme. For a map like System.Account, the key is the twox_64 hash of the pallet name and item name concatenated with the blake2_256 hash of the account address. The @polkadot/api library handles this internally when you call .key() on a query object.

You can also subscribe to multiple keys at once by passing an array. This is useful for monitoring a set of accounts or specific storage items in a single subscription, reducing the number of WebSocket messages.

  • Use api.query.system.account(address).key() to get the storage key.
  • Pass an array of keys to state_subscribeStorage.
  • Filtering at the node level reduces data transfer.
// Example: subscribe to balance changes for a specific account
const { ApiPromise, WsProvider } = require('@polkadot/api');

async function main() {
  const provider = new WsProvider('wss://finney-rpc.onfinality.io');
  const api = await ApiPromise.create({ provider });

  const address = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; // example
  const key = api.query.system.account.key(address);

  const unsub = await api.rpc.state.subscribeStorage([key], (items) => {
    items.forEach(({ key, value }) => {
      console.log(`Storage change: ${key} -> ${value}`);
    });
  });

  // Expected output: Storage change: 0x... -> 0x...
}

main().catch(console.error);

Best vs Finalized Heads: Which to Use?

For miners and validators, the best head (new head) is often sufficient for monitoring block production. However, for applications that require finality (e.g., cross-chain bridges, payment confirmations), you should use finalized heads to avoid reorgs.

Bittensor's consensus uses a subjective weight mechanism, but for reading chain state, the distinction is standard Substrate behavior. Use subscribeNewHeads for real-time but potentially reorged data, and subscribeFinalizedHeads for irreversible data.

The finalized head is determined by the GRANDPA finality gadget, which provides deterministic finality after a certain number of blocks. The chain_subscribeFinalizedHeads subscription only emits blocks that have been finalized by GRANDPA, making them safe for irreversible actions.

In contrast, chain_subscribeNewHeads emits the latest block that the node considers best, which may be reorged if a longer chain appears. For monitoring block production, this is usually fine, but for settlement logic, always use finalized heads.

  • Best head: lowest latency, may be reorged.
  • Finalized head: safe for irreversible actions.
  • Choose based on your use case: monitoring vs. settlement.

Common Failures and Troubleshooting Checklist

Even with a robust client, you may encounter issues. Here are common failures and fixes:

If you see 429 Too Many Requests, you're hitting rate limits. Reduce subscription frequency or use a dedicated endpoint. For more, see our Bittensor RPC rate limits and 429s.

Another common issue is receiving 1011 Internal Error from the WebSocket server, which often indicates that the server is closing the connection due to an internal error or policy violation. Check your subscription count and message frequency.

If you see 1008 Policy Violation, it may be due to exceeding connection limits or sending invalid data. Ensure your client sends proper JSON-RPC requests and respects the server's limits.

For a comprehensive list of WebSocket close codes, refer to the IANA WebSocket Close Code Registry.

  • Connection drops: implement reconnection with exponential backoff and resubscribe.
  • Subscription ID mismatch: ensure you handle notifications with the correct subscription ID.
  • Storage key errors: double-check the key generation; use api.query to get the correct key.
  • Timeouts: send a ping every 30 seconds to keep the connection alive.
  • For generic fixes, see generic WebSocket RPC disconnection fixes.

Tradeoffs and Limitations

Public WebSocket endpoints are convenient but have limitations: rate limits, connection caps, and potential instability. For production, consider a dedicated endpoint or your own node. Also, note that Bittensor's mining/registration uses a separate Subtensor flow, not the standard RPC subscriptions.

Provider-specific limits vary; always check your provider's documentation. For OnFinality's service, see API service and RPC pricing.

Public endpoints are shared among many users, so they may experience higher latency and occasional throttling. Dedicated endpoints provide guaranteed resources and are recommended for applications that require consistent performance.

Additionally, WebSocket connections consume server resources, so providers often limit the number of concurrent connections per IP. Using a single connection for multiple subscriptions is more efficient than opening multiple connections.

  • Public endpoints are not for heavy production use.
  • Dedicated endpoints offer higher reliability and lower latency.
  • Understand the difference between reading chain state and mining operations.

Next Steps and Further Reading

Now that you understand Bittensor WebSocket RPC, you can build real-time applications. For more details, explore the official Bittensor docs and Polkadot.js docs.

Check out our Bittensor RPC guidance (RPC Assistant) for quick answers, and the Bittensor Finney network page for endpoint details. For broader learning, visit the OnFinality Learn hub.

To dive deeper into Substrate's RPC methods, the Substrate RPC documentation is an authoritative reference. For WebSocket-specific considerations, the MDN WebSocket documentation provides a good overview of the protocol.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started