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

Reading Bittensor Subnet Hyperparameters over RPC

A practical guide to reading Bittensor subnet hyperparameters over Subtensor RPC, decoding the returned struct, and caching values safely.

TL;DR

Bittensor subnet hyperparameters are the governing parameters of a subnet's incentive mechanism, including tempo, immunity period, max validators, difficulty, weights rate limit, and registration settings. They are chain state, not constants: governance and subnet owners can change them at any block, and they differ per subnet, so clients must read them rather than hard-code them. The preferred read path is the runtime API subnetInfo_getSubnetHyperparams, which returns a single typed struct composed the same way the runtime composes it. Direct state storage reads via state_getStorage are possible but require the exact storage key hashing and SCALE decoding, and raw keys are unstable across runtime upgrades. This guide shows how to read, decode, cross-check, and cache hyperparameters correctly, with runnable Node.js examples and a results table for measuring against your own endpoint.

What a Bittensor Subnet Hyperparameter Is

A subnet hyperparameter is one of the governing parameters of a subnet's incentive mechanism. The set includes tempo (the number of blocks in a subnet's epoch), immunity period (how long a newly registered neuron is protected from deregistration), max validators (the validator permit limit), min and max difficulty, weights rate limit (how often weights may be set), and registration settings such as registration cost and allowed registration blocks. The Bittensor documentation maintains the canonical reference at subnet hyperparameters.

These values are not constants in the protocol. They are chain state: governance can change them, and subnet owners can adjust subnet-specific parameters through the relevant extrinsics. They also differ per subnet, so a value read for one netuid tells you nothing about another. A client that hard-codes tempo or max validators will silently drift out of correctness the first time a parameter changes.

Because hyperparameters are state, the correct client behavior is to read them from the chain at a known block and to treat any cached copy as valid only for that block. The rest of this guide covers the two read paths, how to decode the result, and how to keep a cache honest.

  • Tempo: blocks per epoch for the subnet's incentive mechanism.
  • Immunity period: blocks a new neuron is protected from deregistration.
  • Max validators: the validator permit limit for the subnet.
  • Min/max difficulty: bounds on the proof-of-work difficulty for registration.
  • Weights rate limit: minimum blocks between weight-setting operations.
  • Registration settings: cost and related registration constraints.

Why Hyperparameters Must Be Read, Not Hard-Coded

A client that assumes a fixed tempo will miscompute epoch boundaries the moment governance changes it. A client that assumes a fixed max validators will misreport validator capacity. The same applies to immunity period, difficulty bounds, and weights rate limit. Each of these is a live value that can change at any block.

The practical consequence is that any downstream computation — epoch timing, validator eligibility, registration cost estimation — should be derived from a hyperparameter read taken at a known block, not from a constant in your source. If you need epoch and emission context alongside hyperparameters, the Bittensor subnet tempo, epochs, and emission reads guide covers those reads in detail.

Reading rather than hard-coding also makes your client resilient across subnets. A single code path that reads hyperparameters for a given netuid works for every subnet, whereas per-subnet constants multiply maintenance and drift.

The Runtime API Read Path: subnetInfo_getSubnetHyperparams

The preferred read path is the runtime API subnetInfo_getSubnetHyperparams, which returns a single typed struct containing the subnet's hyperparameters. Because the runtime composes the struct, the fields are assembled the same way the runtime itself uses them, which removes the risk of mis-hashing a storage key or mis-decoding a SCALE value.

Some subtensor releases expose a V2 variant of the method. The method name and version differ across subtensor releases, so treat the exact name as documented / varies by node and confirm it against the node you are querying. The Substrate state_getMetadata and runtime versions guide explains how to inspect runtime metadata to discover which runtime API methods a node exposes.

Call the runtime API at an explicit block when you need reproducibility. Passing a block hash pins the read to that state, which is what you want for caching and for cross-checking against a metagraph snapshot.

  • Returns a single typed struct rather than raw storage bytes.
  • Field composition matches the runtime's own usage.
  • Method name and version vary by subtensor release.
  • Pin to a block hash for reproducible reads.

The Direct Storage Read Path: state_getStorage

The alternative is to read the SubtensorModule storage maps directly with state_getStorage. This requires computing the correct storage key, which in Substrate is derived from the pallet name, the storage item name, and the key arguments, then hashed with the appropriate hasher (twox or blake2, depending on the map). Getting any part of that wrong returns null or garbage.

Even when the key is correct, the returned value is SCALE-encoded and must be decoded against the type defined in the runtime metadata. That type can change across runtime upgrades, which makes raw storage keys and decoders unstable over time. For these reasons, the runtime API path is preferable for correctness, and direct storage reads are best reserved for cases where no runtime API exposes the value you need.

If you do read storage directly, pin the read to a block and record the runtime version alongside the value, so you can detect when a decoder needs updating.

  • Requires exact storage key hashing (twox/blake2) and SCALE decoding.
  • Key layout and value type can change across runtime upgrades.
  • Returns null for a wrong key or an absent value.
  • Prefer the runtime API when one exists.

Assembling a Full Subnet Record with subnetInfo_getSubnetInfo

Hyperparameters are only part of a subnet's state. To assemble a full subnet record, combine the hyperparameters with subnetInfo_getSubnetInfo, which returns owner, emission, and registration cost among other fields. Together these give you the parameters that govern the mechanism plus the economic context around it.

For validator and stake context, the Bittensor staking state over RPC guide covers delegation and stake reads, and Reading Bittensor metagraph state covers the metagraph. A complete subnet view typically joins hyperparameters, subnet info, and a metagraph snapshot taken at the same block.

Joining reads at the same block matters. If you read hyperparameters at block N and the metagraph at block N+1, a parameter change in between can make the two inconsistent. Pin both reads to the same block hash.

Decoding the Returned Hyperparameter Struct

The struct returned by subnetInfo_getSubnetHyperparams contains the subnet's governing fields. Decode each field according to its type: tempo and immunity period are block counts, max validators is a count, difficulty bounds are numeric, and weights rate limit is a block interval. Registration settings are typically numeric or boolean depending on the field.

Some fields are subnet-specific, so a missing field is not necessarily an error. A subnet may not use every parameter, and a runtime release may add or rename fields. Treat absent fields as 'not applicable or not exposed by this node' rather than as a decode failure, and log the runtime version alongside the decoded struct.

Because the struct is typed, you can decode it with the runtime metadata rather than hand-rolling a decoder. This is the main correctness advantage of the runtime API path over raw storage reads.

  • Tempo and immunity period: block counts.
  • Max validators: validator permit count.
  • Min/max difficulty: numeric bounds.
  • Weights rate limit: block interval.
  • Missing fields may be subnet-specific, not errors.

Runnable Node.js Example: Reading Hyperparameters over WebSocket

The example below connects to a Subtensor WebSocket endpoint, calls the runtime API for a subnet's hyperparameters, prints the struct fields, and re-reads after a new block. Replace the endpoint and netuid with your own values. The endpoint is provider-specific; see the Bittensor RPC guide (RPC Assistant) for endpoint options and the Bittensor Finney network page for network context.

The script uses a minimal JSON-RPC 2.0 client over WebSocket. It pins the read to the latest block hash so the value is reproducible, and it subscribes to new blocks to trigger a re-read. This is the pattern to use in a long-running service.

const WebSocket = require('ws');

const ENDPOINT = 'wss://your-subtensor-endpoint';
const NETUID = 1;

let id = 0;
function call(ws, method, params) {
  return new Promise((resolve, reject) => {
    const reqId = ++id;
    const onMsg = (data) => {
      const msg = JSON.parse(data);
      if (msg.id !== reqId) return;
      ws.off('message', onMsg);
      if (msg.error) reject(new Error(JSON.stringify(msg.error)));
      else resolve(msg.result);
    };
    ws.on('message', onMsg);
    ws.send(JSON.stringify({ jsonrpc: '2.0', id: reqId, method, params }));
  });
}

async function readHyperparams(ws) {
  const hash = await call(ws, 'chain_getBlockHash', []);
  const params = await call(ws, 'state_call', [
    'SubnetInfoRuntimeApi_get_subnet_hyperparams',
    '0x' + NETUID.toString(16).padStart(8, '0'),
    hash
  ]);
  console.log('block', hash);
  console.log('hyperparams (SCALE hex)', params);
  return { hash, params };
}

(async () => {
  const ws = new WebSocket(ENDPOINT);
  await new Promise((r) => ws.on('open', r));
  await readHyperparams(ws);
  await call(ws, 'chain_subscribeNewHeads', []);
  ws.on('message', async (data) => {
    const msg = JSON.parse(data);
    if (msg.method === 'chain_newHead') {
      await readHyperparams(ws);
    }
  });
})();

Detecting a Stale Cache and Re-Reading on Schedule

A cached hyperparameter value is only valid for the block at which it was read. To detect staleness, store the block hash and runtime version alongside the value, and re-read on a schedule or on a new-block subscription. If the runtime version changes, invalidate the cache and re-decode.

A practical pattern is to re-read hyperparameters every N blocks (where N is small relative to tempo) and to force a re-read whenever the runtime version changes. For a subnet with a short tempo, a shorter re-read interval is appropriate; for a long tempo, a longer interval is fine. The key is to bound the maximum staleness rather than to assume the value never changes.

If you serve hyperparameters to other services, expose the block hash with the value so consumers can decide whether the value is fresh enough for their use case.

  • Store block hash and runtime version with the value.
  • Re-read on a schedule or on new-block subscription.
  • Invalidate the cache when the runtime version changes.
  • Expose the block hash to downstream consumers.

Cross-Checking Hyperparameters Against the Metagraph

A useful cross-check is to verify that the number of active validators in the metagraph respects max_validators. Read the metagraph at the same block as the hyperparameters and count validator permits. If the count exceeds max_validators, either your read is stale or you are counting the wrong field.

The same approach applies to immunity period: a neuron registered within the immunity window should not be deregistered. Cross-checking against the metagraph catches decode errors and stale caches that a single read would miss. The Reading Bittensor metagraph state guide covers the metagraph fields you need.

Cross-checks are not a substitute for correct decoding, but they are a cheap way to detect drift in a running service.

Results Table: Measuring Against Your Own Endpoint

Because endpoint behavior and runtime versions vary, measure against your own endpoint rather than relying on published numbers. The table below is a template to fill in with your own observations. Record the endpoint, the block hash, the runtime version, and the decoded fields for each read.

Run the read at several blocks and compare. If a field changes without a runtime version change, that is a governance or subnet-owner change and your cache should have caught it. If a field fails to decode, record the runtime version and check the metadata for the current type.

  • Endpoint: your Subtensor WebSocket or HTTP URL.
  • Block hash: the block the read was pinned to.
  • Runtime version: from state_getRuntimeVersion.
  • Tempo, immunity period, max validators: decoded values.
  • Weights rate limit, min/max difficulty: decoded values.
  • Cross-check: active validators vs max validators.

Limitations, Tradeoffs, and Troubleshooting

Hyperparameters are chain state that governance and subnet owners can change at any block. The runtime API method name and version differ across subtensor releases, so treat the exact name as documented / varies by node and confirm it against your endpoint. Raw storage keys are unstable across runtime upgrades, and some fields are subnet-specific, so a missing field is not necessarily an error.

Common failure modes: a null result from state_getStorage usually means a wrong storage key or an absent value; a decode failure usually means the runtime type changed; a stale value usually means the cache was not invalidated on a runtime version change. For endpoint and connectivity issues, see the Bittensor RPC guide (RPC Assistant).

For production services, prefer the runtime API path, pin reads to a block, store the runtime version, and cross-check against the metagraph. If you need managed endpoints, the API service and RPC pricing pages describe options, and the OnFinality Learn hub collects related guides.

  • Method name/version varies by node: confirm against metadata.
  • Raw storage keys unstable across runtime upgrades.
  • Missing fields may be subnet-specific, not errors.
  • Null storage result: wrong key or absent value.
  • Decode failure: runtime type changed.
  • Stale value: cache not invalidated on version change.

Next Steps for Building a Hyperparameter-Aware Client

Start by reading hyperparameters for a single netuid at a pinned block, then decode the struct using runtime metadata. Add a new-block subscription and re-read on a bounded interval. Store the block hash and runtime version with each value, and expose them to consumers.

Once the read path is stable, join it with subnetInfo_getSubnetInfo and a metagraph snapshot at the same block to assemble a full subnet record. Use the cross-checks described above to detect drift. For related reads, see the Bittensor subnet tempo, epochs, and emission reads and Bittensor staking state over RPC guides.

Finally, treat the hyperparameter read as a versioned contract: when the runtime version changes, re-verify your decoder and your cache invalidation logic. That discipline is what keeps a client correct as the network evolves.

Never Worry about Infrastructure Again

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

Get Started