Solana Blockhash — Why It Expires and How to Handle It Right

What Solana blockhashes are, why they expire, and how to manage them in production transactions. The patterns that work, the patterns that silently lose transactions.

BoltTx Team··7 min read
solanablockhashtransactionrpcsendtransaction

If you've debugged enough Solana transaction failures, you've eventually arrived at "the blockhash expired." It sounds boring; it's actually one of the most common silent failure modes for production Solana code. Most bot operators we've talked to have lost meaningful trades to blockhash expiry without realising it for weeks.

This piece covers what blockhashes are, why they expire, what to do about it, and the specific patterns that distinguish robust production code from the brittle stuff that fails at the worst possible moment.

What a Blockhash Is

A Solana blockhash is a recent block's hash, used as part of a transaction's signing context. The transaction is signed against this specific hash, which means:

When you call getLatestBlockhash, you get a recent blockhash to sign against. When you submit the transaction, validators check that the blockhash is still recent enough.

Why It Expires

The 150-block validity window has two purposes:

  1. Replay prevention. A transaction signed last week can't be resubmitted now.
  2. Fee market sanity. Old transactions don't pile up as the network's perception of priority fees evolves.

The window is short on Solana — much shorter than analogous mechanisms on Ethereum (where transactions can sit in the mempool indefinitely). This is a consequence of Solana's fast block times: 60-90 seconds is many slots.

The practical consequence: your transactions need to be submitted with fresh blockhashes, and your retry logic needs to refresh blockhashes between attempts.

How Blockhash Expiry Causes Silent Failures

The pattern that bites most people:

// Don't do this in production.
const tx = new Transaction().add(instruction);
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.sign(payer);

for (let i = 0; i < 5; i++) {
  try {
    const sig = await connection.sendTransaction(tx, [payer]);
    return sig;
  } catch (e) {
    await sleep(2000);  // 2 seconds between retries
  }
}

After 5 retries with 2-second delays, you've spent 10 seconds. That's well within the blockhash validity window — usually. But if the RPC was slow on each attempt, or you got rate-limited and waited longer, you can easily exceed 60 seconds. Your last few retries are being submitted against an expired blockhash. The RPC may surface this as an error, may silently drop, may return success but the transaction never lands.

The result: your bot thinks it submitted; nothing landed; you don't know why.

The Pattern That Works

Refresh blockhashes between retry attempts:

async function sendWithRetry(buildTx, connection, signers, maxAttempts = 2) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    // Fresh blockhash for each attempt
    const { blockhash, lastValidBlockHeight } =
      await connection.getLatestBlockhash("confirmed");

    const tx = buildTx(blockhash);
    tx.sign(...signers);

    try {
      const sig = await connection.sendTransaction(tx, signers, {
        skipPreflight: true,
        maxRetries: 0,
      });

      // Wait for confirmation, with the lastValidBlockHeight as the deadline
      const confirmed = await waitWithDeadline(
        connection, sig, lastValidBlockHeight, 30_000
      );
      if (confirmed) return sig;
    } catch (e) {
      // Log, decide whether to continue
    }
  }
  throw new Error("Transaction failed after retries");
}

Key points:

Choosing the Right Commitment for Blockhashes

getLatestBlockhash accepts a commitment parameter:

For most production code, "confirmed" is right. You want a blockhash that's stable enough not to be reorganised but still recent enough that you have plenty of validity window left.

"processed" blockhashes are tempting for latency-sensitive code (you get the freshest possible), but the rare cases where they get invalidated cost you whole transactions. Not worth the optimisation for most workloads.

Common Blockhash Anti-Patterns

Things we've seen in production code that cause problems:

Pre-fetching blockhashes. Code that calls getLatestBlockhash once at startup and reuses it. Works for the first few transactions; breaks after a minute.

Caching blockhashes. Some code caches blockhashes for "5 seconds to reduce RPC calls." Often the cached blockhash is older than that by the time it's used.

Building transactions long before signing. If you build a transaction (with blockhash) and queue it to be signed and submitted later, the blockhash is already old by the time you submit.

Not handling "blockhash not found" errors. The RPC tells you the blockhash isn't recognised; many bot codebases just retry with the same hash instead of refreshing.

Excessive simulation before submission. Simulating a transaction takes time; if you simulate too many times before submitting, the blockhash you simulated against may no longer be valid.

Long retry chains. "I retry up to 10 times with 1 second between attempts." That's 10+ seconds of latency, well into blockhash expiry territory.

Special Cases: Durable Nonces

For long-running transactions where blockhash expiry is structurally a problem, Solana has durable nonces. These are dedicated nonce accounts that don't expire on the blockhash window — useful for offline signing, multi-sig flows, or any case where signing happens significantly before submission.

Brief overview:

// Create a nonce account
const noneAccount = await createNonceAccount(connection, payer);

// Use the nonce as the recent blockhash
const tx = new Transaction({ feePayer: payer.publicKey, recentBlockhash: nonceValue });
tx.add(SystemProgram.nonceAdvance({ noncePubkey: noneAccount, authorizedPubkey: payer.publicKey }));
tx.add(yourActualInstruction);

For 99% of trading bot use cases you don't need durable nonces — you should just use fresh recent blockhashes. Durable nonces are for cases where the gap between signing and submission is measured in minutes or hours.

For the dedicated treatment, see our durable nonce documentation.

What to Do This Week

If you've been seeing transactions silently fail or land inconsistently:

  1. Audit your retry logic. Are you reusing blockhashes across retries? If yes, that's likely the cause.
  2. Set maxRetries: 0 on your sendTransaction calls. Manage retries yourself with fresh blockhashes.
  3. Use "confirmed" commitment for getLatestBlockhash unless you have a specific reason for "processed".
  4. Stop retrying past 2 attempts. If it didn't land in two tries, it's not landing.
  5. Add per-signature telemetry. When a transaction fails, log the blockhash, attempt count, and the lastValidBlockHeight at submission. Lets you see which failures are blockhash-related.
  6. For long-signing flows, consider durable nonces. Otherwise, refresh blockhashes aggressively.

What BoltTx Provides

BoltTx handles transaction submission with the right defaults:

import { Connection } from "@solana/web3.js";

const connection = new Connection(
  "https://bolttx.io/?api-key=YOUR_API_KEY",
  "processed"
);

Free tier signup. Run real workload for a week and compare your blockhash-related failure rate.

FAQ

How long does a blockhash stay valid? About 150 blocks, roughly 60-90 seconds depending on network speed.

What's the difference between blockhash and lastValidBlockHeight? The blockhash is the value you sign against; the lastValidBlockHeight is the block number after which this blockhash is no longer accepted. Use both for proper retry logic.

Should I always use "confirmed" commitment for getLatestBlockhash? Yes for production. "processed" is faster but can be invalidated by reorgs. The rare invalidation cost outweighs the latency saving.

Does setting maxRetries higher help? No — it usually causes silent expiry of blockhashes during the retry chain. Set it to 0 and manage retries yourself.

Are blockhash issues more common during congestion? Yes. Congestion delays inclusion, which means blockhashes are more likely to expire before the transaction lands.

Further Reading

Back to all posts