A Solana RPC can return a transaction signature before the transaction lands.
Treating that response as payment confirmation creates a quiet class of
production bugs: stuck checkouts, duplicate actions, false success screens, and
support tickets with no useful trace.
This guide gives you a blockheight-aware submission protocol for Solana
applications. It covers confirmation, retries, expiration, priority fees, and
safe re-signing. Examples use current Solana RPC concepts and @solana/kit
terminology.
A signature is a tracking ID, not a receipt
The sendTransaction RPC method submits a signed transaction to an RPC node. A
successful response returns the first signature embedded in the transaction. It
does not prove that a leader processed the transaction or that the cluster
finalized it.
Your application should model submission as a state machine:
- created: the transaction message has a recent blockhash and a fee payer.
- signed: every required signer has approved the exact message.
- submitted: an RPC accepted the serialized transaction and returned its
signature.
- processed: a leader included it in a block.
- confirmed: a supermajority voted on a block containing it.
- finalized: the cluster finalized the containing block.
- expired: the blockheight passed the transaction's last valid blockheight
without the required confirmation.
Only your product requirements can decide which state counts as success. A
low-risk interface update may accept confirmed. A withdrawal or order
settlement may require finalized plus an application-level state check.
Recent blockhashes give every transaction a deadline
A normal Solana transaction includes a recent blockhash. When you call
getLatestBlockhash, the response also includes lastValidBlockHeight. Keep
both values with the transaction record.
Validators accept a recent blockhash within the latest 151 stored hashes,
according to Solana's confirmation guide. That protocol boundary, rather than a
browser timer, determines when a standard transaction expires.
A wall-clock timeout cannot replace that deadline. Block production can vary,
and elapsed time cannot prove whether the original transaction may still land.
The last valid blockheight can.
Keep one commitment when you fetch the blockhash, simulate the transaction, and
evaluate confirmation. Mixed commitment levels can produce confusing results
when RPC nodes observe different forks or slots.
import { createSolanaRpc } from '@solana/kit';
const rpc = createSolanaRpc(process.env.SOLANA_RPC_URL!);
const commitment = 'confirmed' as const;
const latest = await rpc.getLatestBlockhash({ commitment }).send();
const lifetime = {
blockhash: latest.value.blockhash,
lastValidBlockHeight: latest.value.lastValidBlockHeight,
};Store signature, blockhash, lastValidBlockHeight, commitment, submission
time, attempt count, and the operation's own idempotency key. Those fields turn
a vague "transaction failed" report into a trace you can investigate.
The Safe Resubmission Protocol
The Safe Resubmission Protocol for a standard recent-blockhash transaction is:
- Fetch a blockhash and its last valid blockheight at your chosen commitment.
- Build and simulate the complete message, including compute-budget
instructions.
- Sign once.
- Submit the same signed bytes.
- Poll signature status while checking the current blockheight.
- Rebroadcast the same signed bytes within the original validity window when
needed.
- Stop when the required commitment is reached, an on-chain error appears, or
the blockheight passes the deadline.
- Rebuild and request a new signature only after the old blockhash has expired.
The distinction between rebroadcasting and re-signing matters. Rebroadcasting
the same signed bytes cannot create a second distinct transaction. Rebuilding
with a fresh blockhash creates a new transaction that can also execute.
Before re-signing, prove that the first transaction expired or prove through
application state that repeating the operation is safe. Otherwise, both versions
may land.
Poll status and blockheight together
WebSocket subscriptions improve response time, but production clients still need
an HTTP fallback. A dropped connection should not leave the interface spinning
forever.
The following example shows the control flow. Adapt the response types and error
handling to the exact Solana Kit version pinned by your application.
type ConfirmationResult =
| { status: 'confirmed' }
| { status: 'failed'; error: unknown }
| { status: 'expired' };
async function waitForConfirmation({
rpc,
signature,
lastValidBlockHeight,
}: {
rpc: ReturnType<typeof createSolanaRpc>;
signature: string;
lastValidBlockHeight: bigint;
}): Promise<ConfirmationResult> {
while (true) {
const [statuses, blockHeight] = await Promise.all([
rpc.getSignatureStatuses([signature]).send(),
rpc.getBlockHeight({ commitment: 'confirmed' }).send(),
]);
const status = statuses.value[0];
if (status?.err) return { status: 'failed', error: status.err };
if (
status?.confirmationStatus === 'confirmed' ||
status?.confirmationStatus === 'finalized'
) {
return { status: 'confirmed' };
}
if (blockHeight > lastValidBlockHeight) {
return { status: 'expired' };
}
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
}The loop classifies the outcome. Product actions stay outside it. Your checkout,
swap, or mint flow can then display pending, failed, expired, and confirmed
states accurately.
Retries should reuse the signed transaction
RPC nodes have their own rebroadcast behavior, and sendTransaction accepts a
maxRetries option. You still need an application policy because RPC defaults
do not know your user experience, deadline, or operation risk.
While the blockhash remains valid, rebroadcast the original serialized
transaction. Fetching a new blockhash for every retry creates a new message. A
request timeout alone does not justify another wallet signature.
A sensible policy has bounded attempts, a short delay with jitter, and an
alternate RPC path for critical flows. Record which endpoint accepted each
attempt. Keep preflight enabled for the first submission unless measurements
justify skipping it.
The preflight phase checks signatures, verifies the blockhash, and simulates the
transaction against the selected bank. Skipping it may reduce one round trip,
but it also removes an early error signal. That trade belongs in a measured
latency budget, not a copied code snippet.
Fees and idempotency protect different layers
Priority fees cannot repair a broken transaction
A prioritization fee can improve scheduling priority during contention. It
cannot fix an invalid account, a stale blockhash, a failed program instruction,
or weak retry logic.
Solana calculates the priority fee from the requested compute-unit limit and
compute-unit price. The fee is based on the requested limit, not the compute
units your transaction eventually consumes. Oversized limits can therefore waste
SOL.
At the July 21, 2026 review, Solana's fee documentation lists a base fee of
5,000 lamports per signature. Fee parameters can change, so read the current
network documentation instead of freezing that value in product logic.
Estimate compute use, add a safety margin that reflects observed variance, and
set the price from current network conditions. Revisit both values when the
instruction mix changes.
import {
estimateAndSetComputeUnitLimitFactory,
estimateComputeUnitLimitFactory,
} from '@solana/kit';
const estimate = estimateComputeUnitLimitFactory({ rpc });
const estimateAndSet = estimateAndSetComputeUnitLimitFactory(estimate);
const messageWithLimit = await estimateAndSet(transactionMessage);Simulation is an estimate. Stateful programs may consume a different amount when
the transaction executes. Track compute failures and adjust the margin from your
own production data.
Idempotency belongs above the chain
Blockheight-aware confirmation prevents unsafe re-signing, but it cannot define
your business operation. Give each user intent a stable idempotency key, such as
an order ID or withdrawal ID.
Persist that key before submission. On retry or page reload, recover the
existing transaction record instead of starting a new operation. After
confirmation, verify the expected state transition on-chain or in your indexed
data before marking the business action complete.
For a custom program, store or derive enough state to reject a repeated
operation. For a composed transaction that calls programs you do not control,
make the off-chain coordinator detect prior completion before preparing another
signature request.
That boundary matters most for payments. A transaction can succeed on-chain
while your browser loses the response. The next page load should reconcile state
instead of charging again.
Failure modes worth testing before launch
The RPC accepted the transaction, but it never landed
Return a pending state, continue status checks, and rebroadcast the same bytes
within the validity window. Never label submission as payment success.
The wallet signed after the blockhash expired
Check validity before submission when wallet approval may take time. If the
blockhash expired, rebuild and ask for a fresh signature with a clear
explanation.
The first request timed out
A timeout is an unknown outcome. Query the signature before constructing
anything new.
Confirmation and simulation use different commitments
Standardize commitment in one transaction policy object. Pass it to blockhash
fetching, simulation, status checks, and confirmation.
The fee was high, but the program still failed
Inspect simulation logs and the on-chain error. Priority affects scheduling, not
program correctness.
A user refreshes during confirmation
Recover the transaction record by wallet and idempotency key. Resume status
checks from the stored signature and deadline.
Know when this runbook is the wrong tool
Recent-blockhash transactions suit interactive flows where a user can sign and
submit promptly. Approvals that may remain offline for hours or days need a
different lifetime model. Durable nonces support long-lived or offline signing
and require nonce-specific confirmation rules.
One slow development transaction does not justify custom rebroadcast
infrastructure. Start with the SDK's send-and-confirm helpers, instrument the
flow, and add control when product requirements or measured failure rates demand
it.
Treating finalized as a universal default also adds avoidable latency. Choose
commitment from the consequence of acting before finality.
Production checklist
Before shipping a transaction flow, verify each item:
- Submission success and confirmation success are separate states.
- The transaction record stores its signature and last valid blockheight.
- Blockhash fetch, simulation, and confirmation share a commitment policy.
- Initial submission keeps preflight enabled unless measurements justify
otherwise.
- Retries rebroadcast identical signed bytes while the blockhash remains valid.
- Re-signing happens only after expiry or a proven idempotent state check.
- The interface recovers pending transactions after refresh or reconnect.
- Compute limits come from simulation plus a measured margin.
- Priority fees respond to current conditions and have a cost ceiling.
- On-chain errors, expiry, RPC timeouts, and dropped subscriptions are
observable.
- The business operation has a stable idempotency key.
- Your tests include slow signing, expired blockhashes, RPC failure, and
duplicate clicks.
FAQ
Does a returned signature mean the transaction succeeded?
The RPC accepted the submitted transaction and returned its identifier. Check
signature status until you reach the commitment required by your product.
Should I retry with a new blockhash immediately?
Rebroadcast the original signed transaction while its blockhash remains valid.
Rebuilding too early can create two valid transactions for one user action.
Should every application wait for finalization?
Finalization is appropriate when the cost of acting early exceeds the extra
latency. Document the commitment choice per operation.
Should I set `skipPreflight` to improve landing speed?
Keep preflight enabled by default. Skip it only after testing shows a meaningful
benefit and your system replaces the lost validation and diagnostics.
Do priority fees guarantee inclusion?
Priority fees influence scheduling. Delivery, confirmation, and successful
program execution still depend on separate parts of the transaction path.
Primary references
- Solana: Retrying Transactions
- Solana: Transaction Confirmation and Expiration
- Solana RPC: sendTransaction
- Solana RPC: getSignatureStatuses
- Solana RPC: isBlockhashValid
- Solana: Fee Structure
- Anza: Solana Kit
Next step: Book a Solana transaction reliability review through the
Builderz project fit form. Include the affected flow, RPC setup,
logs, and acceptance target.



