Best Practices
Optimize your Solana RPC integration for low latency, high reliability, and cost efficiency with these proven patterns and techniques.
Connection Pooling
FalconQ uses undici connection pooling with 128 connections and 30s keepalive. When using @solana/web3.js, the Connection class reuses TCP connections automatically. Avoid creating multiple Connection instances.
// GOOD: Single connection instance, reused across your app
import { Connection } from '@solana/web3.js'
const connection = new Connection(
'https://api.falconq.xyz/v1/rpc',
{
httpHeaders: { Authorization: 'Bearer fq_rpc_live_YOUR_KEY' },
commitment: 'confirmed',
}
)
export default connection
// BAD: Creating a new connection per request
async function getBalance(address) {
const conn = new Connection('https://api.falconq.xyz/v1/rpc', {
httpHeaders: { Authorization: 'Bearer fq_rpc_live_YOUR_KEY' },
}) // Creates new TCP connection every time!
return conn.getBalance(address)
}Batch Requests with getMultipleAccounts
Fetch multiple accounts in a single request instead of calling getAccountInfo repeatedly. This reduces latency and conserves rate limit quota.
// BAD: N requests for N accounts
const account1 = await connection.getAccountInfo(addr1)
const account2 = await connection.getAccountInfo(addr2)
const account3 = await connection.getAccountInfo(addr3)
// 3 requests, 3 round trips
// GOOD: 1 request for N accounts
const accounts = await connection.getMultipleAccountsInfo(
[addr1, addr2, addr3],
{ commitment: 'confirmed' }
)
// 1 request, 1 round trip
// Advanced: Batch fetch token account balances
const tokenAccounts = await connection.getTokenAccountsByOwner(
walletAddress,
{ programId: TOKEN_PROGRAM_ID },
)
const mints = tokenAccounts.value.map(({ account }) => {
const info = account.data.parsed.info
return new PublicKey(info.mint)
})
const mintInfos = await connection.getMultipleAccountsInfo(mints)
// 2 total requests for ALL tokens in the walletChoose the Right Commitment Level
Different use cases need different commitment levels. Using confirmed instead of finalized can reduce latency by 200-400ms for most operations.
| Use Case | Recommended | Why |
|---|---|---|
| Trading / MEV | confirmed | Fastest confirmation without significant rollback risk |
| Balance display | finalized | Users expect accurate, non-reverting balances |
| Transaction confirmation | confirmed | Balance of speed and safety for user-facing flows |
| Real-time streaming | processed | Lowest latency, highest throughput |
| Settlement / withdrawals | finalized | Irreversible operations require maximum safety |
Error Handling & Retries
Implement robust retry logic with exponential backoff for transient failures. Never retry 4xx errors (client errors) but always retry 5xx and network errors.
async function rpcWithRetry(
connection,
method,
args,
{ maxRetries = 5, baseDelay = 500 } = {}
) {
for (let i = 0; i <= maxRetries; i++) {
try {
return await connection[method](...args)
} catch (err) {
const isRetryable =
err.message?.includes('429') ||
err.message?.includes('503') ||
err.message?.includes('timeout') ||
err.message?.includes('fetch') ||
err.message?.includes('ECONNRESET')
if (!isRetryable || i === maxRetries) throw err
const delay = baseDelay * Math.pow(2, i) + Math.random() * 200
console.warn(
`RPC ${method} failed (attempt ${i + 1}/${maxRetries + 1}), retrying in ${delay}ms: ${err.message}`
)
await new Promise(r => setTimeout(r, delay))
}
}
}
// Usage
const balance = await rpcWithRetry(
connection, 'getBalance', [publicKey]
)
const latestBlockhash = await rpcWithRetry(
connection, 'getLatestBlockhash', []
)Prefer WebSocket over Polling
For real-time data, use WebSocket subscriptions instead of polling. WebSocket is more efficient and receives updates instantly.
// BAD: Polling — wastes requests, stale data
setInterval(async () => {
const account = await connection.getAccountInfo(addr)
console.log(account?.lamports)
}, 2000) // 30 requests/min just for one account!
// GOOD: WebSocket — efficient, real-time
ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'accountSubscribe',
params: [addr.toBase58(), { commitment: 'confirmed', encoding: 'jsonParsed' }]
}))
ws.on('message', (data) => {
const msg = JSON.parse(data.toString())
if (msg.method === 'accountNotification') {
console.log('Updated:', msg.params.result.value.lamports)
}
})Transaction Building Patterns
Always fetch the latest blockhash before building transactions and set compute unit limits to avoid unexpected costs.
import {
Connection, Transaction, ComputeBudgetProgram,
PublicKey, SystemProgram, LAMPORTS_PER_SOL
} from '@solana/web3.js'
async function buildTransfer(connection, from, to, amountSol) {
// 1. Get latest blockhash (valid for ~90 seconds)
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash('confirmed')
// 2. Build transaction with compute budget
const tx = new Transaction()
.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 50_000 }))
.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }))
.add(SystemProgram.transfer({
fromPubkey: from.publicKey,
toPubkey: new PublicKey(to),
lamports: amountSol * LAMPORTS_PER_SOL,
}))
tx.recentBlockhash = blockhash
tx.feePayer = from.publicKey
// 3. Sign and send
tx.sign(from)
const signature = await connection.sendRawTransaction(
tx.serialize(),
{
skipPreflight: false,
preflightCommitment: 'confirmed',
maxRetries: 3,
}
)
// 4. Confirm with block height check
await connection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
'confirmed'
)
return signature
}Dynamic Priority Fees
Estimate priority fees dynamically based on recent blocks to ensure your transactions land without overpaying.
import { ComputeBudgetProgram } from '@solana/web3.js'
async function getPriorityFee(connection) {
// Fetch recent prioritization fees (last 150 slots)
const fees = await connection.getRecentPrioritizationFees()
if (fees.length === 0) return 10_000 // Default fallback
// Sort and pick the 75th percentile to balance landing vs cost
const sorted = fees
.map(f => f.prioritizationFee)
.sort((a, b) => a - b)
const idx = Math.floor(sorted.length * 0.75)
const fee = sorted[idx]
return Math.max(fee, 5_000) // Minimum 5,000 micro-lamports
}
async function buildTxWithDynamicFee(connection, instructions) {
const priorityFee = await getPriorityFee(connection)
const tx = new Transaction()
.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFee }))
.add(...instructions)
return tx
}Monitor Your Latency
FalconQ is served from Frankfurt. For latency-critical bots, host your workload close to the region and use the response headers to separate network distance from processing time.
// Measure where time is spent on each request
const start = performance.now()
const response = await fetch('https://api.falconq.xyz/v1/rpc', {
method: 'POST',
headers: {
Authorization: 'Bearer fq_rpc_live_YOUR_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getSlot' }),
})
const total = performance.now() - start
const proxyMs = Number(response.headers.get('x-proxy-latency'))
const upstreamMs = Number(response.headers.get('x-upstream-latency'))
const networkMs = total - proxyMs - upstreamMs
console.log({ total, proxyMs, upstreamMs, networkMs })
// High networkMs => move your workload closer to Frankfurt
// High upstreamMs => consider lighter methods or cachingClient-Side Caching
Cache immutable data like account metadata, token mint info, and program IDs locally to avoid redundant RPC calls.
const cache = new Map()
const TTL_MS = 30_000
async function getCachedAccountInfo(connection, address) {
const key = address.toBase58()
const cached = cache.get(key)
if (cached && Date.now() - cached.ts < TTL_MS) {
return cached.data
}
const data = await connection.getAccountInfo(address)
cache.set(key, { data, ts: Date.now() })
return data
}
// Cache token mint info (never changes for existing mints)
const mintCache = new Map()
async function getTokenDecimals(connection, mint) {
const key = mint.toBase58()
if (mintCache.has(key)) return mintCache.get(key)
const info = await connection.getParsedAccountInfo(mint)
const decimals = info.value?.data.parsed.info.decimals ?? 0
mintCache.set(key, decimals)
return decimals
}Security Checklist
Never expose API keys in client-side code. Always proxy RPC calls through your backend.
Use environment variables for keys. Never commit API keys to version control.
Validate all on-chain data. Do not trust account data without verification. Check program IDs, account owners, and data layout.
Set compute unit limits. Without limits, a malicious transaction could consume excessive compute and fail unexpectedly.
Handle stale blockhashes. Always check the lastValidBlockHeight and retry if the blockhash expires before confirmation.
Monitor for rate limits. Watch for 429 responses and implement backoff. Sudden rate limit spikes may indicate a runaway loop.