system_dryRun is a Substrate JSON-RPC method that applies an extrinsic or XCM message against the node's current storage overlay and returns either Ok(weight consumed) or Err(a DispatchError with post-info), without writing to chain state. It is a safe pre-submission check: it tells you whether a call would succeed and how much weight it costs, but it does not enforce transaction-pool validity, consume a nonce, pay a fee, or emit events. Because the dry run is evaluated at one block and the real submission executes at a later block, a successful dry run is necessary but not sufficient for a successful submission. This guide shows how to call system_dryRun with @polkadot/api, parse the text-encoded Result, compare the returned weight against a payment_queryInfo fee estimate, and interpret the output as an allow/warn/deny decision.
What system_dryRun Does Against Current State
system_dryRun is a Substrate JSON-RPC method that applies an extrinsic or an XCM message against the node's current storage overlay and returns the result without committing any state change. The node executes the call as if it were being included in a block, measures the weight consumed, and then discards the overlay. Nothing is written to chain state, no event is emitted, and no nonce is consumed. The polkadot.js Substrate JSON-RPC reference documents the method and its variants at polkadot.js.org/docs/substrate/rpc.
The return value is a text-encoded Result. On success it is Ok(weight), where weight is the weight consumed by the call. On failure it is Err(a DispatchError with post-info), which carries the dispatch error and the weight that was consumed before the error occurred. The client must parse this text encoding rather than treating the response as a plain JSON object. Because the call is applied against the node's current storage overlay, the result reflects the state at the block the node is currently importing or the block you specify with the optional at parameter.
This makes system_dryRun a safe way to learn whether a call would succeed and how much weight it costs before you spend a nonce or pay a fee. It is the pre-submission counterpart to reading the extrinsic pool, which is covered in Polkadot extrinsic pool and pending transactions.
- Applies an extrinsic or XCM message against the current storage overlay.
- Returns Ok(weight) on success or Err(DispatchError with post-info) on failure.
- Does not write to chain state, emit events, consume a nonce, or pay a fee.
- Evaluated at one block; the real submission executes at a later block.
Argument Forms and the Optional at Block Hash
The argument to system_dryRun has two forms depending on the node version. In one form you pass an encoded extrinsic (the signed or unsigned extrinsic bytes). In the other you pass a hexadecimal encoded call (the call bytes without the extrinsic envelope). The polkadot.js Substrate JSON-RPC reference documents both variants. Because the encoding differs across client versions, you should confirm which form your target node expects before building the request. The optional at parameter lets you pin the dry run to a specific block hash; if omitted, the node uses its current head.
When you pin at to a block hash, the dry run is evaluated against the state at that block. This is useful for reproducibility: you can re-run the same simulation against the same block and compare results. It is also useful when you want to check a call against a known-good state rather than a moving head. The tradeoff is that a dry run at an old block may not reflect the state the real submission will execute against.
If you are unsure which block hash to use, you can read the current head with chain_getHeader and chain_getBlockHash. The method for reading block data at a specific block is described in Reading Polkadot block data at a specific block.
- Form 1: encoded extrinsic bytes.
- Form 2: hexadecimal encoded call bytes.
- Optional at parameter pins the dry run to a specific block hash.
- Encoding differs across client versions; confirm before building the request.
How dryRun Differs from Submitting an Extrinsic
Submitting an extrinsic and waiting for an event is a different operation from a dry run. A real submission enters the transaction pool, is validated against pool rules, consumes a nonce, pays a fee, and emits events when included in a block. A dry run does none of these things. It does not enforce transaction-pool validity, does not consume a nonce, does not pay a fee, and does not emit events. The polkadot.js Substrate JSON-RPC reference documents the method's return shape, and the Polkadot developer documentation at docs.polkadot.com covers extrinsics, weights, and dispatch semantics.
This means Ok from a dry run is necessary but not sufficient for a successful real submission. A call can pass a dry run and still fail at submission time because the pool rejects it, because the nonce is stale, because the fee cannot be paid, or because the state changed between the dry run and the real execution. The dry run tells you about dispatch outcome and weight, not about pool admission or fee payment.
The practical consequence is that you should treat a dry run as one input into a submission decision, not as a guarantee. Pair it with a fee estimate and a nonce check before signing. The fee estimation workflow is covered in Polkadot payment_queryInfo and fee estimation.
- Dry run: no pool validation, no nonce, no fee, no events.
- Real submission: pool validation, nonce consumption, fee payment, events.
- Ok from dry run is necessary but not sufficient for a successful submission.
- State can change between the dry run and the real execution.
Using Returned Weight to Sanity-Check a Fee Estimate
The weight returned by a successful dry run is the weight the call would consume. You can use it to sanity-check a fee estimate from payment_queryInfo. If the fee estimate implies a weight that is wildly different from the dry run weight, something is inconsistent: the estimate may be based on a different call, a different block, or a different weight limit. The payment_queryInfo method returns a fee estimate derived from weight and length, and the derivation is described in Polkadot payment_queryInfo and fee estimation.
You can also use the dry run weight to set a weight limit before signing. If you set the weight limit too low, the call will fail with an overweight error even though the dry run succeeded. If you set it too high, you may overpay or hit a block limit. The dry run gives you a measured weight to anchor the limit. Note that the weight returned by the dry run is the weight consumed at the block you simulated against; the real execution may consume a different amount if the state differs.
Because the dry run does not pay a fee, the weight it returns is not a fee. It is an input to fee calculation. Treat it as a measurement, not a charge.
- Compare dry run weight against the weight implied by payment_queryInfo.
- Use dry run weight to set a weight limit before signing.
- A too-low weight limit causes an overweight failure despite a successful dry run.
- Dry run weight is a measurement, not a fee.
XCM and Contract Call Simulation Differences
system_dryRun has variants for different payload types. One variant simulates an XCM message, and another simulates a contract call. The difference matters because the payload encoding and the error surface differ. An XCM dry run returns XCM-specific errors, while a contract call dry run returns contract-specific errors. The polkadot.js Substrate JSON-RPC reference documents the variants. You should match the variant to the payload you intend to submit.
For XCM, the dry run applies the message against the current state and returns the weight consumed or an XCM error. For a contract call, the dry run applies the call against the contract's storage and returns the weight consumed or a contract error. In both cases the dry run does not commit state. The practical benefit is the same: you learn whether the payload would succeed and how much weight it costs before you submit.
If you are working with a chain that exposes these variants, check the node's metadata or the polkadot.js reference for the exact method names. The encoding differs across client versions, so a payload that works on one node may need a different encoding on another.
- XCM variant returns XCM-specific errors and weight.
- Contract call variant returns contract-specific errors and weight.
- Match the variant to the payload you intend to submit.
- Encoding differs across client versions.
Decoding a DispatchError Before Spending a Nonce
When a dry run fails, it returns Err(a DispatchError with post-info). The DispatchError tells you why the call would fail. Decoding it before you submit lets you fix the call without spending a nonce or paying a fee. The error handling workflow is covered in Decoding Polkadot extrinsic dispatch errors.
The post-info in the error carries the weight consumed before the error occurred. This is useful for understanding how far the call got before failing. It is not a fee, because the dry run does not pay a fee. It is a measurement of the work done before the error.
Common DispatchError variants include BadOrigin, Module errors, and other runtime-specific errors. The exact set depends on the runtime. You should decode the error against the runtime metadata for the chain you are targeting. If the error is a Module error, the module index and error index identify the specific failure.
- Err carries a DispatchError and post-info weight.
- Decode the error before submitting to avoid spending a nonce.
- post-info is the weight consumed before the error, not a fee.
- Decode against the runtime metadata for the target chain.
Runnable Node.js Example: Dry Run and Fee Comparison
The following Node.js example connects to a Polkadot WebSocket endpoint, encodes a call with @polkadot/api, calls system_dryRun at the current head, parses the resulting weight, and compares it to a payment_queryInfo fee estimate. Replace the endpoint with your own provider endpoint. The example uses a simple balances transfer call; adapt the call to your use case.
The example assumes you have @polkadot/api installed. It uses the api.rpc.system.dryRun method, which wraps the system_dryRun RPC. The result is a text-encoded Result, so the example parses it with the api's registry. The example also calls payment_queryInfo to get a fee estimate and prints both for comparison.
const { ApiPromise, WsProvider } = require('@polkadot/api');
async function main() {
const provider = new WsProvider('wss://your-polkadot-endpoint');
const api = await ApiPromise.create({ provider });
// Build a call: transfer 1 DOT to a recipient.
const recipient = '15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5';
const amount = '10000000000'; // 1 DOT in plancks
const call = api.tx.balances.transferKeepAlive(recipient, amount);
// Get the current head block hash.
const head = await api.rpc.chain.getHeader();
const at = head.hash.toHex();
// Dry run the call at the current head.
const dryRunResult = await api.rpc.system.dryRun(call.toHex(), at);
console.log('dryRun raw:', dryRunResult.toString());
// Parse the text-encoded Result.
const parsed = api.registry.createType('Result<Weight, DispatchError>', dryRunResult);
if (parsed.isOk) {
const weight = parsed.asOk;
console.log('dryRun weight:', weight.toString());
} else {
const err = parsed.asErr;
console.log('dryRun error:', err.toString());
}
// Get a fee estimate for the same call.
const info = await api.rpc.payment.queryInfo(call.toHex(), at);
console.log('payment_queryInfo:', info.toString());
await api.disconnect();
}
main().catch(console.error);Runnable curl Example: Raw system_dryRun Request
If you prefer to call the RPC directly, the following curl example sends a raw system_dryRun request. Replace the endpoint and the encoded call with your own values. The call field is the hexadecimal encoded call. The at field is optional; if omitted, the node uses its current head.
The response is a JSON-RPC 2.0 object whose result is a text-encoded Result. You must parse the result string to extract the weight or the DispatchError. The JSON-RPC 2.0 specification defines the request and response envelope, and the Ethereum JSON-RPC specification is a useful reference for the same envelope semantics in a different ecosystem.
curl -sS -H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "system_dryRun",
"params": [
"0x...encoded-call...",
"0x...block-hash..."
]
}' \
https://your-polkadot-endpointReproducible Checklist for Allow, Warn, or Deny Decisions
Use the following checklist to turn a dry run into a submission decision. The checklist is reproducible: run it against your own endpoint and record the results in a table. The table columns should include the call, the block hash, the dry run result, the dry run weight, the payment_queryInfo estimate, and the decision. Fill the table with your own measurements; do not rely on numbers from this article.
The decision categories are allow, warn, and deny. Allow means the dry run succeeded and the weight is within your limit. Warn means the dry run succeeded but the weight is close to your limit or the fee estimate is inconsistent. Deny means the dry run failed with a DispatchError, or the weight exceeds your limit, or the fee estimate cannot be paid.
Record the block hash for each run so you can reproduce it. If you re-run against a different block, the result may differ. The checklist is a method, not a guarantee.
- Run system_dryRun at a pinned block hash and record the result.
- Parse Ok(weight) or Err(DispatchError with post-info).
- Compare the dry run weight against your weight limit.
- Compare the dry run weight against the payment_queryInfo estimate.
- Decide allow, warn, or deny based on the comparison.
- Record the block hash, call, result, weight, estimate, and decision in a table.
Limitations and Tradeoffs of Dry Run Simulation
The most important limitation is that a dry run is evaluated against the state at one block, while the real submission executes at a later block. State-dependent calls can still fail after a successful dry run. For example, a transfer can fail if the sender's balance changes between the dry run and the real execution. A governance call can fail if the proposal state changes. The dry run is a snapshot, not a prediction.
Some nodes restrict or disable system_dryRun. This is documented behavior that varies by node. A node operator may disable the method for performance, security, or policy reasons. If the method is unavailable, you cannot use it as a pre-submission check. You should confirm availability against your target endpoint before relying on it.
The argument encoding differs across client versions. A payload that works on one node may need a different encoding on another. You should confirm the expected form before building the request. Finally, dry run does not model transaction-pool priority or replace-by-fee behavior. A call that passes a dry run may still be dropped from the pool or replaced by a higher-fee transaction. For pool behavior, see Polkadot extrinsic pool and pending transactions.
- Dry run is evaluated at one block; real execution happens later.
- State-dependent calls can still fail after a successful dry run.
- Some nodes restrict or disable system_dryRun; availability varies by node.
- Argument encoding differs across client versions.
- Dry run does not model pool priority or replace-by-fee.
Troubleshooting Common system_dryRun Failures
If system_dryRun returns a method-not-found error, the node may not expose the method or may expose it under a different name. Check the node's metadata and the polkadot.js Substrate JSON-RPC reference. If the method is disabled, you cannot use it; consider a different endpoint or a different pre-submission check.
If the dry run returns Err(DispatchError), decode the error against the runtime metadata. Common causes include BadOrigin, insufficient balance, and module-specific errors. The decoding workflow is covered in Decoding Polkadot extrinsic dispatch errors. If the error is a Module error, the module index and error index identify the specific failure.
If the dry run succeeds but the real submission fails, the state likely changed between the dry run and the submission. Re-run the dry run at the current head and compare. If the failure is a pool rejection, check the nonce and the fee. If the failure is an overweight error, increase the weight limit based on the dry run weight. If the failure is a stale nonce, refresh the nonce and re-sign.
- Method not found: check metadata and reference; method may be disabled.
- DispatchError: decode against runtime metadata.
- Real submission fails after successful dry run: state changed; re-run at current head.
- Overweight error: increase weight limit based on dry run weight.
- Stale nonce: refresh and re-sign.
Next Steps for Pre-Submission Simulation
To put system_dryRun into practice, start by confirming that your target endpoint exposes the method. You can use the Polkadot RPC guide (RPC Assistant) to explore the available methods. Then build a small script that dry runs your call at the current head and prints the weight or the DispatchError. Compare the weight against a payment_queryInfo estimate and record the results in a table.
For a managed endpoint, see Polkadot and the API service. For pricing, see RPC pricing. For more guides, see the OnFinality Learn hub.
As you build out your pre-submission workflow, pair the dry run with a fee estimate and a nonce check. The dry run tells you about dispatch outcome and weight; the fee estimate tells you about cost; the nonce check tells you about pool admission. Together they give you a more complete picture than any one check alone.
- Confirm the endpoint exposes system_dryRun.
- Dry run your call at the current head and record the result.
- Compare weight against payment_queryInfo and record the comparison.
- Pair the dry run with a fee estimate and a nonce check.