Solana RPC for Developers — A Practical Guide to Building Right

What developers actually need from Solana RPC. Setup, common patterns, debugging, and the architectural choices that separate dev environments from production.

BoltTx Team··7 min read
solanarpcdeveloperssdkweb3.jsanchor

If you're building on Solana, the RPC is the first piece of infrastructure you touch and the last piece you stop thinking about. There's a temptation to treat it as a commodity — pick whatever shows up first on Google, plug in the URL, get back to writing program logic. That works in development. It rarely works in production. The gap between "my code calls sendTransaction successfully on devnet" and "my code reliably submits on mainnet under load" is mostly RPC choices.

This piece is a practical guide for developers building on Solana — what RPC actually does, how to set it up right, common patterns and anti-patterns, and what to look at when things break.

What Solana RPC Is, in Code Terms

Mechanically, a Solana RPC is an HTTP endpoint that speaks JSON-RPC. Your client (web3.js, the Solana CLI, anchor framework, your own Rust code) makes HTTP calls; the RPC node responds with chain state or accepts transactions for forwarding to validators.

You touch RPC for:

The split that matters in production: read traffic and write traffic have different optimisation profiles. A great read RPC is often not a great write RPC, and vice versa.

Setup: Web3.js, the Default

The minimal setup most tutorials show:

import { Connection, clusterApiUrl } from "@solana/web3.js";

const connection = new Connection(clusterApiUrl("mainnet-beta"));

This works for development. It's wildly inadequate for production. clusterApiUrl("mainnet-beta") returns Solana's public RPC, which is rate-limited, has no SLA, and lags behind reality during congestion. Fine for dev; never for prod.

The pattern for production:

import { Connection } from "@solana/web3.js";

const connection = new Connection(
  process.env.SOLANA_RPC_URL,  // your RPC provider's URL
  {
    commitment: "confirmed",
    confirmTransactionInitialTimeout: 30_000,
    httpHeaders: {
      // Most providers want this
      "Content-Type": "application/json",
    },
  }
);

Use environment variables for the URL. Don't hardcode RPC URLs in your repo — you'll want to swap providers later, and you'll want different URLs for dev/staging/prod.

Reads: Patterns That Work

Common read operations and the patterns that handle them well:

// Single account
const account = await connection.getAccountInfo(publicKey);

// Multiple accounts (one round-trip)
const accounts = await connection.getMultipleAccountsInfo([key1, key2, key3]);

// Program accounts (filter to relevant data)
const accounts = await connection.getProgramAccounts(programId, {
  filters: [
    { dataSize: 165 },  // SPL token account size
    { memcmp: { offset: 0, bytes: ownerKey } },
  ],
});

Use getMultipleAccountsInfo for batched reads. It's one round-trip instead of N. The N-round-trip pattern of looping over getAccountInfo is one of the most common ways developers waste latency budget.

Use filters with getProgramAccounts. Without filters, you can pull megabytes of irrelevant data. Filters happen RPC-side, which is dramatically faster than client-side filtering.

Cache aggressively for slow-changing state. Token mint metadata, program owners, ATA addresses — these don't change every slot. Cache them.

Writes: Patterns That Work

We covered sendTransaction in its own piece. Brief recap:

const signature = await connection.sendTransaction(tx, signers, {
  skipPreflight: true,
  maxRetries: 0,
  preflightCommitment: "processed",
});

For production:

Subscriptions: When to Use Them

WebSocket subscriptions push updates instead of polling. Useful for:

const subId = connection.onAccountChange(
  publicKey,
  (accountInfo, context) => {
    console.log("Account changed at slot", context.slot);
  },
  "confirmed"
);

// When done:
connection.removeAccountChangeListener(subId);

The trade-off: subscriptions are more complex than polling and have failure modes (reconnect logic, subscription leak management) that polling doesn't. Use them when polling would be too slow or too expensive; otherwise stick with polling.

For high-volume event detection (many accounts, many programs), purpose-built streaming subscriptions are more appropriate than the standard WebSocket subscription model. Beyond the scope of this piece but worth knowing about.

Common Patterns by Bot/dApp Type

Different applications use RPC differently:

Wallets. Mostly reads (balances, transaction history, token holdings) with occasional writes (user-initiated swaps). Want generous read pricing and reliable transaction sending.

Indexers / analytics. Heavy read traffic, often using streaming subscriptions. Less write-sensitive. Read-optimised RPC is fine.

Trading bots. Heavy on sendTransaction, latency-sensitive, MEV-exposed. Need a write-optimised RPC with Anti-MEV routing. Read traffic is moderate.

dApps. Mixed read and write. The user-facing nature means failures are more visible than on bots. Reliability and predictability matter more than peak performance.

Backend services. Often a mix; depends on the use case.

The common pattern that works: use one RPC for reads (often a read-optimised provider with good pricing) and a different one for writes (write-optimised, sub-second confirmation, Anti-MEV routing). The mental model is "read RPC is a database; write RPC is a transaction router."

Common Anti-Patterns

Things developers get wrong, in rough order of impact:

Using one RPC for everything. Some providers are good at reads but mediocre at writes; others are write-specialised but read-expensive. Pick separately.

Hardcoding URLs. Always env-var. You'll want to swap.

Not handling rate limiting. Production RPCs rate-limit; your client should retry with backoff, not crash.

Polling at high frequency for slow data. Querying token metadata every second is wasteful. Cache.

Reconnecting websockets aggressively. Some bots reconnect on every transaction. Connection setup is expensive.

Trusting clusterApiUrl in production. It's the public RPC. Fine for dev, not for prod.

Not measuring. Most production bugs come from "we didn't know X was happening." Per-signature telemetry is the fix.

Building without simulation. Before sending real transactions, simulate. Catches most logic bugs before they cost you fees.

Conflating "transaction sent" with "transaction landed." sendTransaction returning a signature doesn't mean it landed. Always confirm.

What to Do This Week

If you're starting a new Solana project:

  1. Pick a read RPC and a write RPC separately. Different optimisation targets.
  2. Use environment variables for URLs. Don't hardcode.
  3. Set sensible defaults on your Connection. commitment: "confirmed", sensible timeouts.
  4. Use getMultipleAccountsInfo instead of looping over getAccountInfo. Single round-trip.
  5. Filter your getProgramAccounts calls. Always.
  6. Build per-signature telemetry early. You'll need it.
  7. Test against simulated load. Reading well in dev doesn't mean reading well at production volume.

What BoltTx Provides for Developers

BoltTx is built specifically for the write side of Solana applications:

Drop-in:

import { Connection } from "@solana/web3.js";

const writeConnection = new Connection(
  "https://bolttx.io/?api-key=YOUR_API_KEY",
  "processed"
);

// Use it for sendTransaction in your codebase
const signature = await writeConnection.sendTransaction(tx, signers, {
  skipPreflight: true,
  maxRetries: 0,
});

Free tier signup. Pair with a read-focused RPC for best of both worlds. Most production teams run this dual-RPC pattern.

FAQ

Should I use Solana CLI commands directly in production? For ops scripts, sure. For application code, use the SDK (web3.js, anchor, solana-web3.rs).

What's the difference between web3.js and anchor? web3.js is the low-level SDK for direct Solana interaction. Anchor is a framework for writing programs and clients with structured account validation. You'll often use both.

Can I use Solana from Python? Yes — solders and solana-py are the main libraries. Less mature than the JS ecosystem but workable.

What's the right read commitment level? "confirmed" for most reads. "processed" if you're optimising latency and can handle reorgs. "finalized" for high-value irrevocable decisions.

Should I run my own RPC node for development? For development, no — use a hosted provider. For production at significant scale, sometimes — but most teams find that managed RPCs beat self-hosting on cost and reliability.

Further Reading

Back to all posts