Optimizing RPC usage can significantly improve performance, reduce costs, and enhance user experience. This guide covers proven techniques for efficient Solana RPC interactions.

Quick Start

Transaction Optimization

Compute Unit Management

1. Simulate to determine actual usage:

const testTransaction = new VersionedTransaction(/* your transaction */);
const simulation = await connection.simulateTransaction(testTransaction, {
  replaceRecentBlockhash: true,
  sigVerify: false
});
const unitsConsumed = simulation.value.unitsConsumed;

2. Set appropriate limits with margin:

const computeUnitLimit = Math.ceil(unitsConsumed * 1.1);
const computeUnitIx = ComputeBudgetProgram.setComputeUnitLimit({ 
  units: computeUnitLimit 
});
instructions.unshift(computeUnitIx); // Add at beginning

Priority Fee Optimization

1. Get dynamic fee estimates:

const response = await fetch(`https://mainnet.helius-rpc.com/?api-key=${API_KEY}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    method: 'getPriorityFeeEstimate',
    params: [{
      accountKeys: ['11111111111111111111111111111112'], // System Program
      options: { recommended: true }
    }]
  })
});
const { priorityFeeEstimate } = await response.json().result;

2. Apply the priority fee:

const priorityFeeIx = ComputeBudgetProgram.setComputeUnitPrice({ 
  microLamports: priorityFeeEstimate 
});
instructions.unshift(priorityFeeIx);

Transaction Sending Best Practices

// Serialize and encode
const serializedTx = transaction.serialize();
const signature = await connection.sendRawTransaction(serializedTx, {
  skipPreflight: true, // Saves ~100ms
  maxRetries: 0 // Handle retries manually
});

Data Retrieval Optimization

Efficient Account Queries

// Use dataSlice to reduce payload size
const accountInfo = await connection.getAccountInfo(pubkey, {
  encoding: 'base64',
  dataSlice: { offset: 0, length: 100 }, // Only get needed data
  commitment: 'confirmed'
});

Token Balance Lookups

// Don't do this - requires N+1 RPC calls
const tokenAccounts = await connection.getTokenAccountsByOwner(owner, {
  programId: TOKEN_PROGRAM_ID
});
const balances = await Promise.all(
  tokenAccounts.value.map(acc => 
    connection.getTokenAccountBalance(acc.pubkey)
  )
);
// ~500ms + (100ms * N accounts)

Transaction History

// Avoid sequential transaction fetching
const signatures = await connection.getSignaturesForAddress(address, { limit: 100 });
const transactions = await Promise.all(
  signatures.map(sig => connection.getTransaction(sig.signature))
);
// ~1s + (200ms * 100 txs) = ~21s

Real-time Monitoring

Account Subscriptions

// Avoid polling - wastes resources
setInterval(async () => {
  const accountInfo = await connection.getAccountInfo(pubkey);
  // Process updates...
}, 1000);

Program Account Monitoring

// Monitor specific program accounts with filters
connection.onProgramAccountChange(
  programId,
  (accountInfo, context) => {
    // Handle program account changes
  },
  'confirmed',
  {
    filters: [
      { dataSize: 1024 },
      { memcmp: { offset: 0, bytes: ACCOUNT_DISCRIMINATOR }}
    ],
    encoding: 'base64'
  }
);

Transaction Monitoring

// Subscribe to transaction logs for real-time monitoring
const ws = new WebSocket(`wss://mainnet.helius-rpc.com/?api-key=${API_KEY}`);

ws.on('open', () => {
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'logsSubscribe',
    params: [
      { mentions: [programId] },
      { commitment: 'confirmed' }
    ]
  }));
});

ws.on('message', (data) => {
  const message = JSON.parse(data);
  if (message.params) {
    const signature = message.params.result.value.signature;
    // Process transaction signature
  }
});

Advanced Patterns

Smart Retry Logic

class RetryManager {
  private backoff = new ExponentialBackoff({
    min: 100,
    max: 5000,
    factor: 2,
    jitter: 0.2
  });

  async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
    while (true) {
      try {
        return await operation();
      } catch (error) {
        if (error.message.includes('429')) {
          // Rate limit - wait and retry
          await this.backoff.delay();
          continue;
        }
        throw error;
      }
    }
  }
}

Memory-Efficient Processing

// Process large datasets in chunks
function chunk<T>(array: T[], size: number): T[][] {
  return Array.from({ length: Math.ceil(array.length / size) }, (_, i) =>
    array.slice(i * size, i * size + size)
  );
}

// Process program accounts in batches
const allAccounts = await connection.getProgramAccounts(programId, {
  dataSlice: { offset: 0, length: 32 }
});

const chunks = chunk(allAccounts, 100);
for (const batch of chunks) {
  const detailedAccounts = await connection.getMultipleAccountsInfo(
    batch.map(acc => acc.pubkey)
  );
  // Process batch...
}

Connection Pooling

class ConnectionPool {
  private connections: Connection[] = [];
  private currentIndex = 0;

  constructor(rpcUrls: string[]) {
    this.connections = rpcUrls.map(url => new Connection(url));
  }

  getConnection(): Connection {
    const connection = this.connections[this.currentIndex];
    this.currentIndex = (this.currentIndex + 1) % this.connections.length;
    return connection;
  }
}

const pool = new ConnectionPool([
  'https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY',
  'https://mainnet-backup.helius-rpc.com/?api-key=YOUR_API_KEY'
]);

Performance Monitoring

Track RPC Usage

class RPCMonitor {
  private metrics = {
    calls: 0,
    errors: 0,
    totalLatency: 0
  };

  async monitoredCall<T>(operation: () => Promise<T>): Promise<T> {
    const start = Date.now();
    this.metrics.calls++;
    
    try {
      const result = await operation();
      this.metrics.totalLatency += Date.now() - start;
      return result;
    } catch (error) {
      this.metrics.errors++;
      throw error;
    }
  }

  getStats() {
    return {
      ...this.metrics,
      averageLatency: this.metrics.totalLatency / this.metrics.calls,
      errorRate: this.metrics.errors / this.metrics.calls
    };
  }
}

Best Practices

Commitment Levels

  • Use for: WebSocket subscriptions, real-time updates
  • Latency: ~400ms
  • Reliability: Good for most applications

Resource Management

Error Handling

// Implement robust error handling
async function robustRPCCall<T>(operation: () => Promise<T>): Promise<T> {
  try {
    return await operation();
  } catch (error) {
    if (error.code === -32602) {
      // Invalid params - fix request
      throw new Error('Invalid RPC parameters');
    } else if (error.code === -32005) {
      // Node behind - retry with different node
      throw new Error('Node synchronization issue');
    } else if (error.message.includes('429')) {
      // Rate limit - implement backoff
      throw new Error('Rate limited');
    }
    throw error;
  }
}

Common Pitfalls to Avoid

Avoid these common mistakes:

  • Polling instead of using WebSocket subscriptions
  • Fetching full account data when only partial data is needed
  • Not using batch operations for multiple queries
  • Ignoring rate limits and not implementing proper retry logic
  • Using finalized commitment when confirmed is sufficient
  • Not closing subscriptions, leading to memory leaks

Summary

By implementing these optimization techniques, you can achieve:

  • 60-90% reduction in API call volume
  • Significantly lower latency for real-time operations
  • Reduced bandwidth usage through targeted queries
  • Better error resilience with smart retry logic
  • Lower operational costs through efficient resource usage

Next Steps

Ready to implement these optimizations? Check out our Transaction Optimization Guide for transaction-specific best practices.