Solana RPC timeouts can occur at the transport or method level. This article explains the causes, provides a Node.js retry pattern for sending transactions, and offers a troubleshooting checklist.
Direct Answer: What Causes Solana RPC Timeouts and How to Handle Them
Solana RPC timeouts are a common pain point for developers. They can stem from network issues, overloaded public endpoints, or heavy queries like getProgramAccounts. The key is to distinguish between transport-level timeouts (connection/read) and method-level timeouts (the RPC method itself takes too long). For transaction sending, you need a retry strategy that goes beyond simple HTTP retries: you must handle ExpiredBlockhashError and use getSignatureStatuses to confirm finality. This article provides a practical guide to diagnosing and fixing timeouts, with a runnable Node.js example.
In short, always set explicit timeouts on your HTTP client, use a reliable RPC provider, and implement a retry loop that respects Solana's blockhash expiration (~2 slots) and transaction fee implications.
- Transport timeouts: connection and read timeouts on the HTTP client.
- Method timeouts: RPC methods that take too long to respond (e.g.,
getProgramAccounts). - Transaction confirmation: use
getSignatureStatuseswith a retry loop, not justsendTransaction.
Understanding Solana RPC Timeout Behavior
Solana RPC endpoints are HTTP/2 JSON-RPC servers. Timeouts can occur at two layers: the transport layer (TCP connection, TLS handshake, or reading the response) and the method layer (the server processing the request). Transport timeouts are typically configured in your HTTP client (e.g., fetch or axios). Method timeouts are not directly configurable; they depend on the server's processing time and the method's complexity.
Public endpoints like api.mainnet-beta.solana.com are often rate-limited and load-balanced, but they can still be slow under heavy load. Clustered endpoints (like those provided by OnFinality) distribute requests across multiple nodes, but even then, certain methods can be slow. For example, getSignaturesForAddress and getBlock can be slow because they scan large amounts of data. getProgramAccounts is notoriously heavy and can cause timeouts if not filtered properly.
When you send a transaction, sendTransaction returns a transaction signature quickly, but that does not mean the transaction is confirmed. Confirmation requires polling getSignatureStatuses until the transaction reaches the desired commitment level (e.g., confirmed or finalized). This is a separate step that needs its own timeout and retry logic.
- Transport timeouts: set
connectTimeoutandreadTimeoutin your HTTP client. - Method timeouts: use
getProgramAccountswith filters to reduce data load. - Transaction confirmation: poll
getSignatureStatuseswith a timeout and retry.
Transport vs. Method Timeouts: What You Need to Know
Transport timeouts are the easiest to handle. In Node.js, you can set them using the AbortController or a library like axios with timeout option. For example, a 10-second timeout is common for RPC calls. If the server does not respond within that time, the request is aborted.
Method timeouts are trickier. The server may accept the request but take a long time to process it. For instance, getProgramAccounts without filters can scan the entire account space, causing timeouts. The Solana documentation recommends using filters and dataSlice to limit the response size. Similarly, getSignaturesForAddress can be slow if the address has many transactions; use limit and before parameters to paginate.
When using a public endpoint, you may also encounter rate limiting, which can manifest as timeouts or HTTP 429 errors. OnFinality's RPC Assistant can help you choose a provider with better performance and reliability.
- Set transport timeouts to avoid hanging requests.
- Optimize method calls with filters and pagination.
- Consider using a dedicated RPC provider to reduce rate limiting and improve consistency.
Why Transaction Sending Needs a Different Retry Strategy
When you send a transaction, the RPC method sendTransaction only broadcasts the transaction to the cluster. It does not guarantee inclusion in a block. The transaction has a blockhash that expires after about 2 slots (roughly 1.6 seconds). If the blockhash expires before the transaction is processed, you get an ExpiredBlockhashError. Therefore, a simple HTTP retry that resends the same transaction will fail because the blockhash is no longer valid.
To reliably send transactions, you must implement a retry loop that: 1) fetches a fresh blockhash, 2) signs a new transaction, 3) sends it, and 4) polls getSignatureStatuses until the transaction is confirmed or a timeout occurs. The @solana/web3.js library provides sendTransaction with a maxRetries option, but it does not handle blockhash refresh automatically. You need to manually handle ExpiredBlockhashError and BlockhashNotFound.
The official Solana documentation on retrying transactions explains that you should use getSignatureStatuses to check the status and resubmit with a new blockhash if necessary. This is critical for production applications.
- Blockhash expires quickly (~2 slots).
sendTransactiononly broadcasts; confirmation requires polling.- Handle
ExpiredBlockhashErrorby refreshing the blockhash and re-signing.
Runnable Example: Node.js Retry with Backoff for Sending Transactions
Below is a complete Node.js script that demonstrates a robust retry strategy for sending a transaction. It uses @solana/web3.js and includes exponential backoff. The script creates a simple transfer transaction, sends it with retries, and polls for confirmation.
To run it, install dependencies: npm install @solana/web3.js. Replace the PRIVATE_KEY and RPC_URL with your own. The script will output the transaction signature and confirmation status.
const { Connection, Keypair, SystemProgram, Transaction, LAMPORTS_PER_SOL, sendAndConfirmTransaction } = require('@solana/web3.js');
// Replace with your private key (array of 64 numbers) and RPC URL
const PRIVATE_KEY = [/* ... */];
const RPC_URL = 'https://api.mainnet-beta.solana.com'; // or your OnFinality endpoint
const connection = new Connection(RPC_URL, 'confirmed');
const from = Keypair.fromSecretKey(Uint8Array.from(PRIVATE_KEY));
const to = Keypair.generate().publicKey;
async function sendWithRetry(connection, from, to, amount, maxRetries = 5) {
let retries = 0;
while (retries < maxRetries) {
try {
// Get a fresh blockhash
const { blockhash } = await connection.getLatestBlockhash('confirmed');
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: from.publicKey,
toPubkey: to,
lamports: amount,
})
);
transaction.recentBlockhash = blockhash;
transaction.feePayer = from.publicKey;
// Sign and send
transaction.sign(from);
const signature = await connection.sendRawTransaction(transaction.serialize());
console.log(`Transaction sent: ${signature}`);
// Confirm with timeout
const confirmation = await connection.confirmTransaction(signature, 'confirmed');
if (confirmation.value.err) {
throw new Error(`Transaction failed: ${confirmation.value.err}`);
}
console.log(`Transaction confirmed: ${signature}`);
return signature;
} catch (error) {
if (error.message.includes('ExpiredBlockhashError') || error.message.includes('BlockhashNotFound')) {
console.log('Blockhash expired, retrying with new blockhash...');
} else {
console.error('Error:', error.message);
}
retries++;
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
const delay = Math.pow(2, retries) * 1000;
console.log(`Retrying in ${delay / 1000}s...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}
(async () => {
try {
const signature = await sendWithRetry(connection, from, to, 0.001 * LAMPORTS_PER_SOL);
console.log('Final signature:', signature);
} catch (error) {
console.error('Failed to send transaction:', error.message);
}
})();Expected Output and How to Verify
When you run the script, you should see output similar to:
Transaction sent: 5Ux...
Transaction confirmed: 5Ux...
Final signature: 5Ux...
If the blockhash expires, you'll see 'Blockhash expired, retrying with new blockhash...' followed by a delay. The script will eventually confirm the transaction or throw an error after max retries.
To verify the transaction, you can use the Solana Explorer or getSignatureStatuses to check the status. The script already confirms with confirmed commitment, which is sufficient for most use cases. For finality, you can change the commitment to 'finalized'.
- The script prints the transaction signature and confirmation status.
- You can verify on Solana Explorer using the signature.
- Adjust
maxRetriesand backoff parameters based on your needs.
Common Timeout Sources and Troubleshooting Checklist
Here are common sources of Solana RPC timeouts and how to fix them:
- Heavy
getProgramAccountscalls: Use filters anddataSliceto reduce data. For example,getProgramAccountswith adataSizefilter is much faster.
getBalancepolling: If you pollgetBalancefrequently, consider using WebSocket subscriptions instead of polling.
- Load-shedding and cluster degradation: During high network congestion, RPC nodes may shed load, causing timeouts. Use a reliable provider like OnFinality's Solana network page to mitigate this.
- Incorrect commitment levels: Using
finalizedcan be slower; useconfirmedfor faster responses.
- Network issues: Check your internet connection and firewall settings.
- Rate limiting: Public endpoints often rate-limit; use a dedicated RPC provider to avoid this.
- Optimize
getProgramAccountswith filters. - Use WebSocket subscriptions for real-time data.
- Choose a reliable RPC provider.
- Set appropriate commitment levels.
- Monitor network health.
Tradeoffs and Limitations of Retry Strategies
While retrying with a fresh blockhash is essential, it has tradeoffs. Each retry consumes a new blockhash and may incur transaction fees if the transaction is partially processed. Also, retrying too aggressively can increase load on the network. It's important to balance retry count and backoff delays.
Another limitation is that sendTransaction may return a signature even if the transaction is dropped. Polling getSignatureStatuses is necessary to confirm, but it adds latency. For high-throughput applications, consider using sendAndConfirmTransaction from @solana/web3.js, which handles confirmation internally, but it still has the same blockhash issue.
Finally, no retry strategy can guarantee success if the network is severely degraded. In such cases, it's better to fail fast and alert the user rather than retry indefinitely.
- Retries consume blockhashes and may incur fees.
- Confirmation polling adds latency.
- Fail fast under severe network degradation.
Next Steps and Further Resources
To improve your Solana RPC reliability, consider using a dedicated RPC provider like OnFinality. Our Solana network page offers high-performance endpoints with low latency and high availability. You can also explore our pricing for free and paid tiers.
For more troubleshooting tips, check out our generic RPC timeout diagnosis and fixes guide. If you're building on Solana, our API service provides additional tools and analytics.
We also recommend reading the official Solana documentation on RPC API and retrying transactions for deeper understanding. For performance best practices, see Helius's Solana RPC Optimization guide.
- Use OnFinality for reliable Solana RPC endpoints.
- Read the official Solana docs for RPC and retries.
- Explore Helius's optimization guide.