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

Solana Blockhash Expiry and Durable Nonces: Safe Offline Signing

Why Solana transactions expire via lastValidBlockHeight, and how durable nonces enable safe offline signing.

TL;DR

Every Solana transaction must reference a recent blockhash, which expires after roughly 150 blocks (~75 seconds). If you sign offline or your pipeline is slow, the transaction can be rejected with 'BlockhashNotFound'. Durable nonces replace the recent blockhash with a stored value that only advances when you explicitly allow it, making transactions safe to sign far in advance. This article explains the mechanism, shows how to implement both paths, and provides a troubleshooting checklist.

Direct Answer: Why Your Solana Transaction Expires and How to Fix It

If you've ever built a Solana transaction, waited a few minutes, and then submitted it only to see BlockhashNotFound, you've hit the protocol's built-in expiry mechanism. Every Solana transaction must include a recent blockhash – a hash of a recent block – and that hash is only valid for a limited window. The RPC method getLatestBlockhash returns both a blockhash and a lastValidBlockHeight. Once the network reaches that height, the transaction is considered expired and will be rejected. For most use cases, this window is about 150 blocks, which at Solana's 400ms slot time translates to roughly 60–75 seconds, but the exact number is not a fixed protocol constant – it's the node's recent-blockhash window.

The robust pattern for online signing is: fetch a fresh blockhash, sign immediately, and submit. If the blockhash expires before submission, you must re-fetch and re-sign – you cannot simply retry the same signed transaction. For offline or slow pipelines, however, re-signing isn't possible. That's where durable nonces come in. A durable nonce is a special account that stores a value which only changes when you explicitly advance it. By using a durable nonce in place of the recent blockhash, your transaction no longer depends on wall-clock recency. It remains valid until someone else advances the nonce, which only you (or your authority) can do. This makes durable nonces the standard solution for safe offline signing on Solana.

  • Solana transactions require a recent blockhash to prevent replay and ensure freshness.
  • The blockhash is valid only until lastValidBlockHeight; after that, the transaction is rejected.
  • Durable nonces decouple transaction validity from time, enabling offline signing.
  • Use getLatestBlockhash for online flows; use durable nonces for offline or delayed submission.

The Mechanism: Recent Blockhash and lastValidBlockHeight

Solana's transaction format includes a recent_blockhash field. This hash is used to deduplicate transactions and to ensure that a transaction is not valid indefinitely. When you call getLatestBlockhash, the RPC returns a blockhash and the lastValidBlockHeight – the block height at which that blockhash will no longer be accepted. The node maintains a window of recent blockhashes (approximately the last 150 blocks). If you submit a transaction with a blockhash that is older than that window, the node returns an error: BlockhashNotFound.

The official Solana documentation on transaction confirmation explains that a transaction is only valid while its blockhash is within the node's recent-blockhash window. The sendTransaction RPC method will reject a transaction with an expired blockhash. The getLatestBlockhash method is the recommended way to obtain a fresh blockhash and its validity height. The older getRecentBlockhash method is deprecated but still works; it returns only the blockhash, not the lastValidBlockHeight, so you cannot know exactly when it expires.

The key takeaway: the blockhash is not a timestamp. It's a hash of a recent block, and its validity is tied to the chain's progress. If your node is lagging behind the tip, the blockhash it returns may be older than the network's actual tip, making your transaction expire sooner than expected. This is why it's critical to use a healthy, up-to-date RPC endpoint – see Detecting an RPC node lagging behind the tip for how to check.

  • getLatestBlockhash returns blockhash and lastValidBlockHeight.
  • The recent-blockhash window is approximately the last 150 blocks, but it's not a fixed protocol constant.
  • If you submit after lastValidBlockHeight, you get BlockhashNotFound.
  • A lagging RPC can return a stale blockhash, increasing expiry risk.

The Robust Online Pattern: Fetch, Sign, Send, and Handle Expiry

For most applications, the correct flow is straightforward: fetch a fresh blockhash, build and sign the transaction, and send it immediately. If the transaction is not sent within the validity window, you must re-fetch a new blockhash and re-sign. You cannot simply resubmit the same signed transaction because the signature is over the old blockhash.

When you send a transaction and it fails with BlockhashNotFound, do not retry the same transaction. Instead, rebuild it with a new blockhash. This is especially important for payment or token transfers where a duplicate could cause double-spending if you accidentally resubmit a transaction that was actually confirmed but you didn't see the confirmation.

The following JavaScript example uses @solana/web3.js to demonstrate the pattern. It fetches a blockhash, signs a simple transfer, and sends it. If the transaction expires, it catches the error and rebuilds with a fresh blockhash.

// Online signing with blockhash expiry handling
import { Connection, SystemProgram, Transaction, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const from = Keypair.generate(); // replace with your keypair
const to = new PublicKey('...');

async function sendWithRetry() {
  let blockhashInfo = await connection.getLatestBlockhash();
  const transaction = new Transaction();
  transaction.add(SystemProgram.transfer({
    fromPubkey: from.publicKey,
    toPubkey: to,
    lamports: 0.01 * LAMPORTS_PER_SOL,
  }));
  transaction.recentBlockhash = blockhashInfo.blockhash;
  transaction.feePayer = from.publicKey;
  transaction.sign(from);

  try {
    const signature = await connection.sendTransaction(transaction);
    console.log('Transaction sent:', signature);
  } catch (error) {
    if (error.message.includes('BlockhashNotFound')) {
      console.log('Blockhash expired, retrying with fresh blockhash');
      return sendWithRetry();
    }
    throw error;
  }
}

sendWithRetry();

Durable Nonces: The Offline Signing Solution

Durable nonces are a Solana feature that allows a transaction to be signed without a recent blockhash. Instead, the transaction uses a durable nonce – a value stored in a special account (a NonceAccount). This account is created and owned by a program, and it has an authority that can advance the nonce. When you include a durable nonce in a transaction, you must also include a SystemProgram.advanceNonceAccount instruction, which updates the stored nonce to a new value. This instruction must be signed by the nonce authority, and it also pays a fee.

The key property is that the nonce only advances when the authority explicitly signs an advanceNonceAccount instruction. Therefore, a transaction using a durable nonce does not expire based on time; it only becomes invalid if another transaction has already advanced the nonce. This makes it safe to sign a transaction offline, store it, and submit it later – even days or weeks later – as long as the nonce account remains funded and not already used.

The official Solana Cookbook on durable nonces provides a recipe for creating and using a nonce account. The process involves: creating a nonce account, initializing it with an authority, and then using the nonce in place of the recent blockhash. The @solana/web3.js library provides helper functions like createNonceAccount and NonceAccount to simplify this.

  • A durable nonce is stored in a NonceAccount and can only be advanced by its authority.
  • Transactions using a durable nonce include an advanceNonceAccount instruction.
  • The nonce does not expire based on time, only when it is consumed by another transaction.
  • This enables safe offline signing for air-gapped or scheduled transactions.

Step-by-Step: Creating and Using a Durable Nonce

Here's a complete flow to create a nonce account and use it for an offline transaction. This example uses @solana/web3.js and assumes you have a funded payer account. The steps are: create the nonce account, initialize it, then build a transaction that uses the nonce and includes the advance instruction.

First, create a nonce account. This requires funding the account with enough lamports to be rent-exempt. The createNonceAccount helper does this for you. Then, you can retrieve the nonce value using getNonce.

When you're ready to sign offline, you fetch the current nonce value from the account (or from a previously stored value), build your transaction with that nonce as the recentBlockhash, and add the advanceNonceAccount instruction. Sign it with both the fee payer and the nonce authority. The transaction can then be stored and submitted later.

// Creating and using a durable nonce
import { Connection, SystemProgram, Transaction, Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const payer = Keypair.generate(); // fund this account
const nonceAuthority = Keypair.generate(); // authority for the nonce

// Create a nonce account
const nonceAccount = Keypair.generate();
const tx = new Transaction().add(
  SystemProgram.createNonceAccount({
    fromPubkey: payer.publicKey,
    noncePubkey: nonceAccount.publicKey,
    authority: nonceAuthority.publicKey,
    lamports: await connection.getMinimumBalanceForRentExemption(80), // NonceAccount size
  })
);
tx.feePayer = payer.publicKey;
await connection.sendTransaction(tx, [payer, nonceAccount]);

// Fetch the nonce value
const nonceInfo = await connection.getNonce(nonceAccount.publicKey);
const nonce = nonceInfo.nonce;

// Build an offline transaction (e.g., a transfer)
const transfer = SystemProgram.transfer({
  fromPubkey: payer.publicKey,
  toPubkey: someDestination,
  lamports: 0.1 * LAMPORTS_PER_SOL,
});

const offlineTx = new Transaction().add(
  SystemProgram.advanceNonceAccount({
    noncePubkey: nonceAccount.publicKey,
    authorizedPubkey: nonceAuthority.publicKey,
  }),
  transfer
);
offlineTx.recentBlockhash = nonce;
offlineTx.feePayer = payer.publicKey;

// Sign offline (e.g., in an air-gapped environment)
offlineTx.sign(payer, nonceAuthority);

// Later, submit the signed transaction
const signature = await connection.sendTransaction(offlineTx);
console.log('Offline transaction sent:', signature);

Expected Outputs and a Results Table to Fill In

When you run the online example, you should see a signature printed, and the transaction should confirm within a few seconds. If you deliberately delay submission beyond the lastValidBlockHeight, you'll see an error containing BlockhashNotFound. For the durable nonce example, you should see a signature printed even if you wait minutes before submitting, as long as the nonce hasn't been used.

To verify the behavior yourself, you can run the following test and record the results. This table will help you document the expiry times and error messages you observe.

  • Run the online example and note the time between fetching the blockhash and sending the transaction.
  • Deliberately wait 2 minutes before sending to trigger BlockhashNotFound.
  • Run the durable nonce example and wait 5 minutes before sending – it should succeed.
  • Try sending the same durable nonce transaction twice – the second should fail with DurableNonceMismatch.
// Fill in your observations
| Scenario | Wait time | Result (signature/error) |
|----------|-----------|--------------------------|
| Online, immediate send | 0s | |
| Online, 2 min delay | 120s | |
| Durable nonce, 5 min delay | 300s | |
| Durable nonce, double send | - | |

Failure and Fix Checklist

Here are common errors you might encounter and how to fix them. This checklist is based on documented behavior and community reports.

  • BlockhashNotFound: The blockhash expired. Re-fetch a fresh blockhash and re-sign. Do not retry the same transaction.
  • DurableNonceMismatch: The nonce value in your transaction doesn't match the current stored nonce. This usually means the nonce was already advanced by another transaction. Fetch the current nonce and re-sign.
  • Attempt to advance durable nonce: This error occurs when the advanceNonceAccount instruction is not properly signed or the authority is incorrect. Ensure the nonce authority signs the transaction.
  • Nonce account not initialized: You must initialize the nonce account with initializeNonceAccount before using it.
  • Insufficient lamports for rent: The nonce account must be rent-exempt. Fund it with enough lamports (use getMinimumBalanceForRentExemption).
  • RPC lag: If your RPC endpoint is lagging, the blockhash it returns may be stale. Use a healthy endpoint and check for lag – see Detecting an RPC node lagging behind the tip.

Limitations and Tradeoffs of Durable Nonces

While durable nonces solve the offline signing problem, they come with tradeoffs. First, the nonce account must be funded and rent-exempt, which locks up a small amount of SOL. Second, the nonce can only be used once – after it's advanced, you need to fetch a new nonce for the next transaction. This means you must have a process to manage nonce accounts, especially if you sign many transactions offline.

Security-wise, the nonce authority is a critical key. If it's compromised, an attacker can advance the nonce and invalidate your pending transactions. Therefore, the authority should be kept as secure as your main signing keys. Also, note that a durable nonce transaction still requires a fee payer, and the advanceNonceAccount instruction itself incurs a fee.

Finally, durable nonces do not protect against all types of replay. They only ensure that a transaction cannot be replayed after the nonce has been advanced. If you need to prevent replay across different contexts, you still need to design your transaction carefully.

Next Steps and Further Reading

Now that you understand blockhash expiry and durable nonces, you can build more reliable Solana applications. For a deeper dive into Solana's transaction mechanics, see the Solana documentation and the Solana Cookbook.

If you're using an RPC provider, ensure you're using a reliable endpoint. OnFinality offers Solana RPC endpoints with high availability. For more on handling RPC issues, read about Solana RPC timeouts and retries and Decoding Solana transaction simulation errors.

To understand versioned transactions and how they interact with blockhashes, see Solana versioned transactions and parsing. And if you're building on Solana, explore the OnFinality Learn hub for more guides, or check our API service and RPC pricing for production-grade infrastructure.

Never Worry about Infrastructure Again

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

Get Started