Guide

Solana blockhash and transaction expiry explained

You built a transaction, approved it in your wallet, and the network rejected it with blockhash not found or transaction expired. Nothing was stolen — the signature simply aged out. Every Solana transaction must reference a recent blockhash, a fingerprint of a recent slot that proves the tx is fresh and prevents replay attacks. This guide explains how that window works, why transactions die on the shelf, and what wallets and developers do to keep them alive long enough to land.

What a blockhash does

A Solana transaction is not just "move SOL from A to B." It is a signed message that includes a list of instructions, a fee payer, and a recent blockhash — a 32-byte hash tied to a specific slot in the chain's history.

Validators check two things before executing your transaction:

  • Signature validity — the fee payer (and any additional signers) signed this exact byte sequence.
  • Blockhash freshness — the referenced blockhash is still within the allowed lookback window on the ledger.

The blockhash acts as a nonce. Once a transaction with a given signature lands, or once its blockhash falls out of the window, that exact signed message cannot be replayed. Without expiry, an attacker could copy an old signed transfer and submit it again months later.

Blockhashes are not unique per transaction — many txs in the same slot can share the same recent blockhash. Uniqueness comes from the full message content plus signatures.

The ~150-slot expiry window

Solana slots tick roughly every 400 milliseconds. A transaction's blockhash must refer to a slot that is still "recent" when validators process the tx — historically about 150 slots (~60 seconds) of lookback, though the exact rule is expressed in terms of block height rather than wall-clock time.

Modern RPC responses pair the blockhash with lastValidBlockHeight — the highest block height at which validators will still accept a transaction using that hash. Your backend or wallet should track both values:

  • blockhash — goes into the transaction message.
  • lastValidBlockHeight — tells you when to stop retrying and fetch a fresh hash.

If the current chain height passes lastValidBlockHeight before your transaction lands, the network discards it. You need a new blockhash, a new signature, and (usually) a new send — not just a resubmit of the old bytes.

Congestion stretches wall-clock time: a tx can sit in a validator's queue while slots march on. That is one reason busy periods pair blockhash expiry with priority fees — you are racing both the expiry window and competing senders.

Common errors users see

Blockhash not found

The validator looked up the hash you embedded and it is no longer in the recent blockhash queue. Typical causes:

  • You left the wallet approval dialog open too long before confirming.
  • The dApp built the transaction minutes ago and never refreshed it.
  • Your RPC node is lagging behind the network — it handed you a blockhash that mainnet validators have already aged out.
  • You are on the wrong cluster (devnet blockhash submitted to mainnet, or vice versa).

Transaction simulation failed: blockhash not found

Wallets and dApps often simulate before showing you the approval popup. Simulation uses the same freshness rules. A stale blockhash fails preflight even if the underlying instruction logic is fine.

Block height exceeded

Some clients surface this when lastValidBlockHeight has passed. The fix is the same: rebuild with a fresh blockhash and re-sign.

How wallets handle freshness

Browser wallets like Phantom, Solflare, and Backpack typically fetch a fresh blockhash at the moment you click "Confirm," not when the site first constructed the transaction. That is why a slow user approval sometimes still succeeds — the wallet rebuilt the message.

However, wallets do not always rewrite arbitrary partially-signed transactions from third-party flows. If a dApp passes a fully built legacy transaction that sat in memory, the wallet may sign exactly what it received. dApp developers should treat blockhash freshness as their responsibility until the user has signed.

Mobile deep links and hardware-wallet workflows add latency. A QR-based Solana Pay flow that waits for a phone scan can exceed the window if the backend does not regenerate the payment request. Merchants should issue a new blockhash (or a new payment reference) if the customer does not pay within roughly a minute.

Developer patterns: fetch, sign, send

The standard lifecycle on @solana/web3.js and similar SDKs:

  1. Call getLatestBlockhash (or getLatestBlockhashAndContext) on a reliable RPC.
  2. Build the transaction, set recentBlockhash and feePayer.
  3. Simulate if needed — catch program errors before asking the user to sign.
  4. Sign and send immediately; do not cache signed bytes.
  5. While polling for confirmation, watch block height against lastValidBlockHeight. If you are close, prepare a replacement tx.

For server-side senders (games, payment relayers, airdrops), use a dedicated RPC with low lag. Public endpoints that return HTTP 429 under load can leave you with blockhashes that expire before your retry succeeds — see our RPC endpoints guide for fallback patterns.

Durable nonces (advanced)

Programs that must sign transactions hours or days after construction — multisig treasuries, scheduled payroll — can use Solana's durable nonce accounts instead of recent blockhashes. A nonce account stores a value that advances on each use, replacing the short-lived blockhash mechanism. Setup is more complex (rent-exempt nonce account, special instruction ordering), but expiry stops being the bottleneck. Most consumer dApps never need this; reach for it only when business logic genuinely cannot sign within a minute.

Blockhash vs confirmation level

Landing a transaction and finalizing it are different milestones. A tx can pass the blockhash check, execute in slot N, and appear as processed while validators still vote on its permanence.

Blockhash expiry affects whether the tx executes at all — not whether it later rolls back on a rare fork. For high-value settlements, wait for finalized commitment before crediting a user. Our confirmation time guide walks through processed vs confirmed vs finalized, and how payment verification should match your risk tolerance.

Slots and epochs — the clock Solana uses to number blocks — are covered in slots and epochs explained. Block height increments once per slot whether or not your transaction was included.

Troubleshooting checklist

  • Retry with a fresh transaction — do not resubmit the same signed bytes after a blockhash error.
  • Confirm mainnet — devnet SOL and devnet blockhashes do not work on live merchants.
  • Check RPC health — switch endpoints if getLatestBlockhash lags or rate-limits.
  • Reduce approval friction — pre-fill forms, simulate early, and avoid multi-step flows that pause on the signing step.
  • During congestion — add a modest priority fee so the tx lands before the window closes.
  • Backend idempotency — if you rebuild and resend, deduplicate by business intent (order ID), not by transaction signature.

Why this design exists

Short-lived blockhashes trade convenience for safety and throughput. Validators do not need an unbounded replay-protection database — they only track a sliding window of recent hashes. Users get fast finality at sub-second slot times; developers get a clear rule: sign close to send time.

Once you internalize the window, "blockhash not found" stops feeling random. It is the network telling you the signed message is too old to execute. Fetch a new hash, sign again, and send — or design your flow so that gap never opens in the first place.

Related reading