Transaction Landing

RPC Infrastructure

Public endpoints (api.mainnet.solana.com) are rate-limited with no SLA. They're fine for development but will fail production payment flows—like trying to run a payment processor through a shared API with no uptime guarantee.

Never use public RPC for production

Use a private RPC provider for reliable, low-latency access.

When choosing an RPC provider, look for:

  • Reliability: SLAs with uptime guarantees (99.9%+)
  • Latency: Geographic proximity to your users
  • Features: Transaction landing features, indexing, priority fee APIs

For a full list of RPC providers, see the RPC Infrastructure Providers guide.

Redundant RPC Configuration

Like any network service provider, RPC providers can experience downtime or periods of degraded performance. To ensure your application is resilient, you should configure your application to use multiple RPC providers.

Solana Kit provides a library for customizing RPC transports which allows you to build your own redundant RPC client. Here's an example of how you might use it to build a redundant RPC client:

import { RpcTransport } from "@solana/rpc-spec";
import { RpcResponse } from "@solana/rpc-spec-types";
import { createHttpTransport } from "@solana/rpc-transport-http";
// Create a transport for each RPC server
const transports = [
createHttpTransport({ url: "https://mainnet-beta.my-server-1.com" }),
createHttpTransport({ url: "https://mainnet-beta.my-server-2.com" }),
createHttpTransport({ url: "https://mainnet-beta.my-server-3.com" })
];
// Create a wrapper transport that distributes requests to them
let nextTransport = 0;
async function roundRobinTransport<TResponse>(
...args: Parameters<RpcTransport>
): Promise<RpcResponse<TResponse>> {
const transport = transports[nextTransport];
nextTransport = (nextTransport + 1) % transports.length;
return await transport(...args);
}

If you prefer not to build your own routing tools, you can leverage a third party service like Iron Forge to handle the routing for you.

Transaction Landing

On Devnet, transactions land fairly easily. On Mainnet, you're competing for block space. To increase the chances of having your transaction included in a block, you should ensure you have properly assembled your transaction. This means:

  • including a fresh blockhash before sending the transaction
  • including a priority fee instruction in the transaction with a competitive priority fee
  • including a compute unit limit instruction in the transaction with a compute unit limit based on the estimated compute units required for the transaction

Additionally, you should consider other tools like Jito Bundles to increase the chances of your transaction being included in a block. Let's explore these tools in more detail.

Transaction Send Configuration

When sending transactions on Mainnet, configure these parameters for optimal landing rates:

Blockhash management:

  • Fetch with confirmed commitment
  • Store the lastValidBlockHeight returned by getLatestBlockhash—this tells you when your transaction expires
  • Blockhashes expire after a time set by the protocol depending on the slot length.

Send options:

  • maxRetries: 0 — Disable automatic RPC retries. Handle retries yourself so you can refresh the blockhash when needed.
  • skipPreflight: true — Bypass simulation before sending. Use this when you've already validated the transaction and want lowest latency. Keep it false during development to catch errors early.
import { createSolanaRpc } from "@solana/kit";
const rpc = createSolanaRpc(process.env.RPC_URL!);
// 1. Get blockhash with confirmed commitment
const { value: latestBlockhash } = await rpc
.getLatestBlockhash({ commitment: "confirmed" })
.send();
// 2. Build and sign your transaction with the blockhash
// ... (transaction building code)
// 3. Send with production settings
const signature = await rpc
.sendTransaction(encodedTransaction, {
encoding: "base64",
maxRetries: 0n, // Handle retries yourself
skipPreflight: true, // Skip simulation for speed (use false during dev)
preflightCommitment: "confirmed"
})
.send();
// 4. Track expiration using lastValidBlockHeight
const { lastValidBlockHeight } = latestBlockhash;
// Stop retrying when current block height exceeds lastValidBlockHeight

Use Priority Fees

Every Solana transaction requires a transaction fee, paid in SOL. Transaction fees are split into two parts: the base fee and the priority fee. The base fee compensates validators for processing the transaction. The priority fee is an optional fee, to increase the chance that the current leader will process your transaction. Think of it like express shipping: you pay more for faster, more reliable delivery.

How fees work:

Total fee = Base fee (5,000 lamports per signature) + Priority fee
Priority fee (legacy/v0) = ceil(compute_unit_price * compute_unit_limit / 1,000,000) lamports
Priority fee (v1 only) = Absolute total in lamports set in the message config

Real-world costs:

  • Simple USDC transfer: ~$0.001-0.005 during normal conditions
  • During congestion: ~$0.01-0.05
  • Peak congestion: Can spike higher

Sample Implementation:

The @solana-program/compute-budget package provides a helper function to easily update or append the compute unit price (in micro-lamports) instruction to a transaction.

import { updateOrAppendSetComputeUnitPriceInstruction } from "@solana-program/compute-budget";
const tx = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayerSigner(payer, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
(m) => appendTransactionMessageInstructions([myInstructions], m),
(m) => updateOrAppendSetComputeUnitPriceInstruction(1000n as MicroLamports, m)
);

Getting fee estimates: Most RPC providers offer priority fee APIs:

For the full fee mechanics, see Transaction Fees and our guide: How to Add Priority Fees to a Transaction.

Optimize Compute Units

Compute on Solana is measured in compute units (CUs) which are a rough estimate of the amount of work the program is doing. Compute limits today are 50 million CUs per block, and 1.4 million CUs per transaction. When you submit a transaction, you need to estimate the amount of compute that will be used, and set the compute unit limit accordingly - this is effectively a request for how much of the total capacity that should be reserved for your transaction. In practice, this means properly estimating the compute units required for your transaction is critical to having your transaction included in a block (and important for managing your priority fees). The transaction and block compute limit are determined by the protocol.

The Solana JSON RPC API has a simulatetransaction method that can be used to estimate the compute units required for a transaction, which includes an estimate of the compute units that will be used. @solana/kit provides helper functions that estimate a transaction's resource limits and set them on the message in one step (using the simulatetransaction method under the hood). For version 1 transactions these helpers also estimate the loaded accounts data size limit.

import {
estimateResourceLimitsFactory,
estimateAndSetResourceLimitsFactory
} from "@solana/kit";
const estimateResourceLimits = estimateResourceLimitsFactory({ rpc });
const estimateAndSetResourceLimits = estimateAndSetResourceLimitsFactory(
estimateResourceLimits
);
const txWithResourceLimits = await estimateAndSetResourceLimits(tx);

If you build and send transactions with a kit plugin client, you typically don't need this step — the client adds compute budget instructions for you when it sends (.sendTransaction()). The manual flow above is for when you assemble transactions directly with @solana/kit.

In production, if you are repeating the same type of transaction multiple times, you should consider caching the compute estimate for the transaction type to avoid the overhead of estimating the compute units every time.

Jito Bundles

Jito bundles are a tool for managing atomic execution of multiple transactions. This is achieved by sending multiple transactions to the Jito network with a tip. Tips can be used to incentivize the Jito network to include your transactions in a block.

Resources:

Retry Strategies

Transactions can fail for many reasons. Unlike traditional payment APIs that return success/failure immediately, blockchain transactions require confirmation tracking.

Key concepts:

  • Blockhash expiration: Blockhash expiration: Transaction blockhashes remain valid for 150 slots (roughly 30 seconds at 200ms slot times). Actual expiration time may vary depending on block production speed. Use lastValidBlockHeight to determine when a blockhash has expired.
  • Idempotency: The same signed transaction always produces the same signature—resubmitting is safe
  • Exponential backoff: Avoid overwhelming the network with rapid retries
import {
createSolanaRpc,
createSolanaRpcSubscriptions,
sendAndConfirmTransactionFactory,
isSolanaError,
SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED
} from "@solana/kit";
const rpc = createSolanaRpc(process.env.RPC_URL!);
const rpcSubscriptions = createSolanaRpcSubscriptions(process.env.RPC_WSS_URL!);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions
});
// Send with automatic confirmation tracking and block height monitoring
try {
await sendAndConfirmTransaction(signedTransaction, {
commitment: "confirmed",
// Optional: abort after 75 seconds
abortSignal: AbortSignal.timeout(75_000)
});
} catch (e) {
if (isSolanaError(e, SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED)) {
// Blockhash expired—rebuild transaction with fresh blockhash and retry
rebuildAndRetryTransaction(); // implement your own logic for rebuilding and retrying the transaction
}
throw e;
}

The sendAndConfirmTransactionFactory from @solana/kit handles confirmation polling and block height tracking automatically. It throws SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED when the transaction's blockhash expires, signaling that you need to rebuild the transaction with a fresh blockhash.

Additional Resources

Error Handling

Solana Kit provides typed errors via isSolanaError(). Use specific error codes instead of string matching:

import {
isSolanaError,
SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED,
SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE,
SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND,
SOLANA_ERROR__INSTRUCTION_ERROR__INSUFFICIENT_FUNDS
} from "@solana/kit";
function handlePaymentError(error: unknown): {
message: string;
retryable: boolean;
} {
// Blockhash expired—rebuild and retry
if (
isSolanaError(error, SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED) ||
isSolanaError(error, SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND)
) {
return { message: "Transaction expired—rebuilding", retryable: true };
}
// Insufficient SOL for fees
if (
isSolanaError(
error,
SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE
)
) {
return { message: "Not enough SOL for fees", retryable: false };
}
// Insufficient token balance
if (
isSolanaError(error, SOLANA_ERROR__INSTRUCTION_ERROR__INSUFFICIENT_FUNDS)
) {
return { message: "Insufficient balance", retryable: false };
}
// Unknown error
console.error("Payment error:", error);
return { message: "Payment failed—please retry", retryable: true };
}

Common Error Codes:

Error CodeCauseRecovery
BLOCK_HEIGHT_EXCEEDEDBlockhash expiredRebuild with fresh blockhash
BLOCKHASH_NOT_FOUNDBlockhash not foundRebuild with fresh blockhash
INSUFFICIENT_FUNDS_FOR_FEENot enough SOLFund fee payer or use fee abstraction
INSUFFICIENT_FUNDSNot enough tokensUser needs more balance
ACCOUNT_NOT_FOUNDToken account missingCreate ATA in transaction

Gasless Transactions

Users expect to pay in stablecoins, not acquire SOL for network fees. Gasless transactions solve this—similar to how Venmo users don't think about ACH fees. See Fee Abstraction for complete implementation.

Is this page helpful?

목차

페이지 편집