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 wallet

Choose 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 CaseRecommendedWhy
Trading / MEVconfirmedFastest confirmation without significant rollback risk
Balance displayfinalizedUsers expect accurate, non-reverting balances
Transaction confirmationconfirmedBalance of speed and safety for user-facing flows
Real-time streamingprocessedLowest latency, highest throughput
Settlement / withdrawalsfinalizedIrreversible 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 caching

Client-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

1.

Never expose API keys in client-side code. Always proxy RPC calls through your backend.

2.

Use environment variables for keys. Never commit API keys to version control.

3.

Validate all on-chain data. Do not trust account data without verification. Check program IDs, account owners, and data layout.

4.

Set compute unit limits. Without limits, a malicious transaction could consume excessive compute and fail unexpectedly.

5.

Handle stale blockhashes. Always check the lastValidBlockHeight and retry if the blockhash expires before confirmation.

6.

Monitor for rate limits. Watch for 429 responses and implement backoff. Sudden rate limit spikes may indicate a runaway loop.