Changing Solana RPC providers is a URL change. That is genuinely all the code requires, and it is why most migrations are done badly.
★The swap is trivial. Knowing whether it helped is the work.★
The Part That Is Actually One Line
const connection = new Connection(NEW_ENDPOINT, "confirmed");
Solana's JSON-RPC interface is standardised, so the same client code works against any compliant endpoint. There is no SDK to change and no data to move.
★That is also the trap.★ Because it is so easy, teams switch on a vendor claim, see no obvious difference, and have no way to tell whether they improved anything or made it worse.
Measure Before, Not After
You cannot evaluate a migration without a baseline, and the baseline has to be collected before you cut over:
async function baseline(connection, samples = 200) {
const results = [];
for (let i = 0; i < samples; i++) {
const submitSlot = await connection.getSlot();
const sig = await sendRepresentativeTransaction(connection);
const landed = await waitForLanding(sig);
results.push(landed.slot - submitSlot);
await sleep(1000);
}
results.sort((a, b) => a - b);
return {
p50: results[Math.floor(results.length * 0.5)],
p90: results[Math.floor(results.length * 0.9)],
p99: results[Math.floor(results.length * 0.99)],
};
}
★Two rules make this measurement honest.★
Measure slot distance, not milliseconds. A faster round trip that does not change which block you land in has changed nothing that matters.
Measure under load, not at a quiet hour. ★Endpoints that look identical when the network is calm diverge under congestion★ — and congestion is when your results are determined.
If you have measured and the gap is in submission rather than reads, a free BoltTx key is one line to test against.
What Actually Differs Between Providers
The JSON-RPC surface is standard; the behaviour around it is not.
| Area | Where they differ |
|---|---|
| Rate limits | ★Per-request vs credit-weighted★ |
getProgramAccounts |
★Often restricted or disabled★ |
| History retention | Recent slots vs months |
| WebSocket limits | Subscriptions per connection |
| Node pool sync | ★Slot skew between nodes★ |
| Priority fee data | Sample window and accuracy |
★The two starred rows cause the most migration surprises.★
Credit-weighted pricing means a request count that fit one provider's limit can blow through another's, because getProgramAccounts may cost many times what getBalance costs.
Slot skew appears as balances that seem to move backwards. Different providers have different pool behaviour, and code that never needed minContextSlot on one endpoint may need it on the next.
Run Both in Parallel First
The safe migration is not a cutover, it is a shadow period:
// ★Old endpoint still authoritative. New one measured only.★
const sig = await oldConnection.sendRawTransaction(raw, opts);
void newConnection
.sendRawTransaction(raw, opts) // ★identical bytes — safe★
.then((s) => shadowMetrics.record(s))
.catch((e) => shadowMetrics.recordError(e));
★Sending identical signed bytes to two endpoints cannot double-execute.★ The signature is the same, and a signature is included at most once — so the second send is either a duplicate the network discards or a faster path to the same inclusion.
This is the one case where parallel submission is free of risk, and it is what makes shadow testing possible at all. Rebuilding for the second endpoint would not be safe.
Cut Over Gradually
const useNew = hash(requestId) % 100 < rolloutPercent;
const conn = useNew ? newConnection : oldConnection;
Move in stages — a small share, then a larger one — and compare the two populations at each step rather than eyeballing a dashboard.
★Keep the old endpoint configured after the cutover completes.★ Health-based failover between them costs nothing while both work and is the difference between a degraded provider being an inconvenience and being an outage.
const endpoints = [primary, secondary];
for (const ep of endpoints) {
if (!health.isHealthy(ep)) continue;
try { return await ep.sendRawTransaction(raw, opts); }
catch (e) { health.recordFailure(ep); }
}
Separate Reads From Sends
The migration is a good moment to stop treating one endpoint as one thing.
★Reads and sends have opposite requirements.★ Reads are heavy, bursty, and tolerant of a slot of staleness. Sends are small, latency-critical, and must not be starved by anything else.
A single shared endpoint means a getProgramAccounts backfill can exhaust the quota your sends need, at precisely the moment your bot is busiest. Splitting them removes an entire class of failure that is invisible until it happens.
What Not to Trust
Vendor latency numbers. Usually round-trip time on a cheap call, measured under favourable conditions. ★Round trip time is not slot distance.★
A quiet-hour comparison. The only interesting question is behaviour under congestion.
Your first hour of data. Cache warmth, connection pooling, and DNS all settle over time.
Averages. ★Two endpoints with identical mean latency can have completely different tails, and losses live in the tail.★ Compare p90 and p99.
What Landing Looks Like
Real transactions through our delivery nodes: median confirmation 336ms — under one slot.
★This is the baseline to compare your own measurement against.★ If your distribution is materially wider after tuning fees and retries, the remaining gap is routing rather than client configuration.
Where BoltTx Fits
We are a send path, not a general RPC. That makes us additive rather than a replacement.
Keep your existing provider for reads, subscriptions, and history. Point only sendRawTransaction at us, which is one line and reversible. Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a transaction is not observable in transit before it lands.
You sign locally. We never hold funds, never sign, and never modify transaction contents — so shadow-testing us with identical bytes alongside your current endpoint is safe. The tip travels inside the transaction, paid on chain from your own wallet, and reverts with the transaction if it fails, because that is how Solana handles atomic transactions. You pay only on transactions that reach the chain.
Get a free API key. No monthly fee:
// Reads stay where they are.
const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
FAQ
How hard is it to switch Solana RPC providers? The code change is one line, since the JSON-RPC interface is standardised. The work is measuring whether the change helped, which requires a baseline collected before you switch.
What should I measure when comparing RPC providers? Slot distance from submission to landing, as a distribution with p50, p90, and p99. Round-trip latency does not tell you whether you made the next block, which is what decides races.
Can I test a new provider without switching? Yes. Send identical signed bytes to both endpoints and record the results from the new one without acting on them. Identical bytes produce one signature, so this cannot double-execute.
Is sending the same transaction to two providers safe? Yes, provided the bytes are identical. A signature is included at most once, so the second submission is either discarded or a faster path to the same inclusion.
What differs between providers if the API is standard?
Rate limit models, whether getProgramAccounts is available, history retention, WebSocket limits, node pool synchronisation, and priority fee data quality. The interface is the same; the behaviour is not.
Why did my request count suddenly exceed limits after migrating? Many providers price by weighted credits rather than raw requests, so heavy calls cost far more than light ones. The same traffic can fit one limit and exceed another.
Why do balances look inconsistent on the new provider?
Different node pool behaviour means consecutive requests can hit nodes at different slots. Pass minContextSlot with the highest slot you have seen to reject stale responses.
Should I keep the old provider after migrating? Yes, as a failover. Health-based selection between two configured endpoints costs nothing while both work and turns a provider degradation into an inconvenience rather than an outage.
How gradually should I cut over? In stages, comparing the two populations at each step. A percentage-based split by request identifier lets you compare like with like rather than judging from a dashboard.
Should reads and sends use the same endpoint? Preferably not. Reads are heavy and bursty while sends are latency-critical, so a shared endpoint lets a backfill exhaust the quota your sends need at the worst moment.
Can I trust a provider's published latency numbers? Treat them as a starting point. They usually report round-trip time on a light call under favourable conditions, which is not the same as slot distance under congestion.
How long should a comparison run? Long enough to include congested periods, not just a quiet window. Caches, connection pools, and DNS also settle over time, so the first hour is not representative.
Why do two endpoints look the same in testing but differ in production? Because testing usually happens at low load. Endpoints diverge under congestion, and congestion is exactly when your fill rate is decided.
Does changing providers require code changes beyond the URL?
Usually not for the send path. You may need minContextSlot for consistency, or adjusted concurrency if the rate limit model differs, but the client code itself is unchanged.
What if the new provider is faster on reads but not on sends? That is common, and it is why separating the two paths is useful. You can keep the faster reads and route sends elsewhere without one decision constraining the other.
How do I know the migration actually helped? Compare slot distance distributions before and after under comparable conditions. If p90 is unchanged, whatever improved did not reach the outcome you care about.