Substrate-based chains like Polkadot expose an upgradeable runtime whose API surface is described by versioned metadata. The state_getMetadata RPC returns this metadata for the current best block or for a specific block hash, letting you decode historical extrinsics and storage correctly. You must pin metadata to the block you are targeting and compare spec_version or metadata hashes to detect upgrades that would otherwise break your client's decoding logic.
Direct Answer: Metadata Is the Chain's API Contract, and It Changes
If you integrate with a Substrate-based chain such as Polkadot, the runtime metadata is the authoritative description of the chain's API: every pallet, callable extrinsic, storage item, event, error, and constant. This metadata is versioned (currently v14 and v15 are common) and changes whenever the runtime is upgraded on-chain. The state_getMetadata RPC method returns this metadata, and you can pass an optional block hash to retrieve the metadata as it existed at that exact block. Failing to pin metadata to your target block is the root cause of silent decoding failures after a runtime upgrade.
The practical rule: always query metadata for the block you are decoding, and cache it keyed by the runtime version (spec_version). Compare the spec_version or metadata hash between requests to detect an upgrade. This article explains the mechanism, shows you how to query metadata at any block, and provides a troubleshooting checklist.
How Runtime Metadata Works: Versioned Schemas for an Upgradeable Runtime
A Substrate (FRAME) chain's runtime is a WebAssembly blob stored on-chain and is upgradeable through governance or sudo. Each runtime upgrade can change the set of pallets, extrinsics, storage keys, events, and errors. The runtime metadata is a machine-readable description of that API surface, encoded using SCALE codec. The metadata format itself is versioned: v14 is the current widely supported format, and v15 adds runtime API information and support for the async backing era. The version is tied to the runtime's spec_version, transaction_version, and apex spec names, which change on upgrades.
Clients built against old metadata will mis-decode extrinsics or storage after an upgrade because the type definitions and indices may have shifted. For example, a call index that pointed to Balances.transfer might now point to a different extrinsic. The Polkadot-SDK documentation and polkadot.js.org are the authoritative primary sources for metadata formats and runtime versioning.
The RPC surface includes state_getMetadata (with optional at block hash), state_call to invoke runtime APIs like Metadata_metadata_at_version, and state_runtimeVersion (or chain_getRuntimeVersion) to get the current spec_version. The availability of these methods depends on the node build and runtime version, as distributed by the chain's runtime state.
Querying Metadata at a Specific Block: The Workflow
To decode a historical extrinsic or storage value, you need the metadata that was valid at the block where that extrinsic was included. The workflow is: 1) Get the block hash of interest (e.g., from a transaction or block number). 2) Call state_getMetadata with that hash as the at parameter. 3) Decode the returned SCALE-encoded metadata to determine its version (v14, v15, etc.) and extract the type registry. 4) Use that registry to decode the extrinsic or storage key.
For the latest block, you can omit the at parameter, but be aware that the node returns metadata for its current best block, which may change between requests. To detect an upgrade between two requests, compare the spec_version from state_runtimeVersion or the metadata hash (e.g., using state_call to Metadata_metadata_at_version and hashing).
polkadot.js exposes version-aware metadata and registry: the @polkadot/api automatically queries metadata and updates its registry when it detects a runtime upgrade (via api.runtimeVersion). However, for historical decoding, you must manually create an Api instance with a specific block hash or use lower-level libraries to decode with the correct metadata.
Runnable Example: Fetch and Compare Metadata at Latest and Historical Blocks
The following bash script uses curl to query a Polkadot RPC endpoint (replace with your own endpoint, e.g., from OnFinality's API service). It fetches the latest block hash, then calls state_getMetadata for the latest and for a specific older block hash (you can replace with a known historical hash). It extracts the metadata version and spec_version using state_call to Metadata_metadata_at_version and state_runtimeVersion.
Expected output shows the metadata version and spec_version for both blocks. If they differ, a runtime upgrade occurred between those blocks. Fill in the results table below with your own measurements.
#!/bin/bash
# Replace with your endpoint
ENDPOINT="https://rpc.polkadot.io"
# Get latest block hash
LATEST_HASH=$(curl -s -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"chain_getBlockHash","params":[],"id":1}' $ENDPOINT | jq -r '.result')
echo "Latest block hash: $LATEST_HASH"
# Fetch metadata at latest
curl -s -H "Content-Type: application/json" -d "{\"jsonrpc\":\"2.0\",\"method\":\"state_getMetadata\",\"params\":[\"$LATEST_HASH\"],\"id\":2}" $ENDPOINT | jq -r '.result' | xxd -r -p | head -c 10 | od -An -t u1
echo ""
# Get runtime version at latest
curl -s -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"state_getRuntimeVersion","params":[],"id":3}' $ENDPOINT | jq '.result.specVersion'
# Replace with an older block hash (e.g., from a known block number)
OLD_HASH="0x..."
# Fetch metadata at old block
curl -s -H "Content-Type: application/json" -d "{\"jsonrpc\":\"2.0\",\"method\":\"state_getMetadata\",\"params\":[\"$OLD_HASH\"],\"id\":4}" $ENDPOINT | jq -r '.result' | xxd -r -p | head -c 10 | od -An -t u1
echo ""
# Get runtime version at old block (using state_call)
curl -s -H "Content-Type: application/json" -d "{\"jsonrpc\":\"2.0\",\"method\":\"state_call\",\"params\":[\"Core_version\",\"0x\",\"$OLD_HASH\"],\"id\":5}" $ENDPOINT | jq '.result'Results Table: Fill in Your Measurements
Run the script above and record the metadata version and spec_version for each block. The metadata version is the first byte of the SCALE-encoded metadata (e.g., 14 for v14, 15 for v15). The spec_version is a number returned by the runtime version call.
- Latest block hash: [fill in]
- Metadata version at latest: [fill in]
- Spec_version at latest: [fill in]
- Old block hash: [fill in]
- Metadata version at old block: [fill in]
- Spec_version at old block: [fill in]
- Did the spec_version change? [yes/no]
Troubleshooting Checklist: Common Failures and Fixes
When querying metadata at a block, you may encounter several issues. Use this checklist to diagnose and fix them.
If you get a 'Cannot convert' error or type-definition drift, you are likely using the latest metadata to decode a historical extrinsic. Fix: fetch metadata at the target block and use that registry.
If storage keys do not match after an upgrade, the pallet prefixes or hashers may have changed. Fix: compare the metadata at the two blocks to identify changes.
If an archive node cannot expose metadata for deeply pruned blocks, it may be because the node is not an archive node or the runtime state is unavailable. Fix: use a dedicated archive node (see Querying Polkadot historical state over RPC).
If state_getMetadata returns an error for an old block, the block might be before the metadata version was introduced or the node does not have that state. Fix: verify the block is within the node's pruning window or use an archive endpoint.
If you see a mismatch between state_getMetadata and state_runtimeVersion, remember that the latter returns the current runtime version, not necessarily the one at the block you are querying. Always pass the block hash to both methods if available.
Limitations and Tradeoffs of At-Block Metadata Queries
Querying metadata at a specific block is powerful but has limitations. First, not all nodes are archive nodes; a full node may prune historical state, making metadata for old blocks unavailable. Second, the metadata format itself evolves, so you must handle multiple versions (v14, v15, future) in your decoder. Third, the RPC method availability varies by node build and runtime version; for example, state_call to Metadata_metadata_at_version may not be present on older runtimes.
Performance-wise, fetching metadata for every block is inefficient. Instead, cache metadata per runtime version and refresh only when the spec_version changes. This is how polkadot.js handles runtime upgrades: it listens for spec_version changes and updates its registry lazily.
Finally, metadata describes the runtime API, but it does not include the actual logic. For decoding errors, you may need to combine metadata with the runtime version and the extrinsic format version (e.g., signed extensions).
Next Steps: Deepen Your Substrate Integration Knowledge
Now that you understand metadata and runtime versions, explore related topics to strengthen your integration. For a broader overview of Polkadot RPC methods, see the Polkadot RPC guide. To understand how finality affects block availability, read about Polkadot finality and the finalized head.
If you are dealing with historical data, the guide on Querying Polkadot historical state over RPC is essential. For handling errors in extrinsics, see Decoding Polkadot extrinsic dispatch errors. And for efficient real-time data, review the Polkadot WebSocket RPC in depth.
For production use, consider using a reliable RPC provider like OnFinality's API service with RPC pricing that suits your needs. Always test your metadata handling against a testnet before deploying to mainnet.