Solana RPC: WebSocket vs HTTP for High-Frequency Bots
A surprising amount of production Solana code does this in a tight loop:
while (running) {
const accounts = await connection.getMultipleAccountsInfo([
raydiumPool, orcaPool, jupiterRoute, ...
]);
decideAndAct(accounts);
await sleep(100);
}
Polling every 100ms means 10 requests per second per pool. With 20 pools you're at 200 RPS just to watch state — before you've placed a single transaction. And on top of being expensive, you're inherently behind: by the time your poll comes back, the account state is already up to 200ms old.
WebSocket subscriptions on Solana solve this. The RPC pushes you the new state the moment a slot lands, not on your polling schedule. You go from 200 RPS to ~0, and your reaction time drops from "polling interval + RTT" to "RTT only" — typically a 5-10× latency reduction for state-change-driven bots.
This post walks through the five Solana subscription methods that matter, where HTTP is still the right tool, the commitment-level gotcha that confuses everyone, and the reconnect logic you absolutely cannot skip.
The five subscriptions worth knowing
Solana RPC exposes a bunch of WebSocket subscriptions. In practice you only ever use five:
| Method | What it pushes | Typical use |
|---|---|---|
accountSubscribe |
New account data whenever the account is written | Watching specific pool state, oracle prices, user balances |
programSubscribe |
Every account owned by a program when it changes | Indexing all positions in a DEX, all open orders on Serum-style books |
logsSubscribe |
Program logs from confirmed transactions | Decoding events emitted by a contract, MEV detection |
signatureSubscribe |
A single notification when a specific tx confirms | "Did my transaction land?" without polling getSignatureStatuses |
slotSubscribe |
Slot updates as the leader produces them | Sequencing logic, fork detection, timing-sensitive code |
The shape of every subscription response is the same: { context: { slot }, value: <the actual thing> }. The slot is critical for reasoning about ordering.
The 5-minute HFT example
Here's a price-watcher bot that monitors three liquidity pools and reacts to state changes. Both styles, side by side:
Polling style (what people write first)
import { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection(
"https://rpc.swiftnodes.io/rpc/solana?key=YOUR_API_KEY",
"confirmed"
);
const pools = [POOL_A, POOL_B, POOL_C].map(p => new PublicKey(p));
setInterval(async () => {
const accounts = await connection.getMultipleAccountsInfo(pools);
accounts.forEach((acc, i) => {
if (acc) checkPrice(pools[i], acc.data);
});
}, 200);
// 5 RPS, ~200ms reaction delay even when nothing changed
Subscription style (what you should write)
import { Connection, PublicKey } from "@solana/web3.js";
const wsConnection = new Connection(
"https://rpc.swiftnodes.io/rpc/solana?key=YOUR_API_KEY",
{
commitment: "confirmed",
wsEndpoint: "wss://rpc.swiftnodes.io/ws/solana?key=YOUR_API_KEY",
}
);
const pools = [POOL_A, POOL_B, POOL_C].map(p => new PublicKey(p));
const subIds = pools.map(pool =>
wsConnection.onAccountChange(pool, (account, ctx) => {
checkPrice(pool, account.data); // fires only when state actually changes
}, "confirmed")
);
// Later, to unsubscribe:
// subIds.forEach(id => wsConnection.removeAccountChangeListener(id));
The second version sends 3 messages total (the initial subscribes), then listens. Network traffic and latency both drop dramatically.
When HTTP is still the right tool
WebSocket subscriptions are not a universal upgrade. Cases where HTTP still wins:
- One-shot reads at startup. Fetching the current state of an account once doesn't justify a subscription. Use
getAccountInfo. - Simulating transactions before submitting.
simulateTransactionis request-response by nature. - Reading historical state. Subscriptions are forward-looking only; for
getAccountInfoat a specific slot orgetTransactionof an old signature, you need HTTP. - Batched read across many accounts you check rarely.
getMultipleAccountsInfofor 100 accounts every 10 seconds is more efficient as a single batched HTTP call than 100 long-lived subscriptions. - Confirming a transaction was successful.
signatureSubscribeexists, but a singlegetSignatureStatusescall after a short delay is usually simpler.
The rule of thumb: if you're polling something more than once per second, switch to a subscription. If you're checking it once every few seconds or less, polling is fine.
The commitment-level gotcha
This one trips people up constantly. Solana has three commitment levels and they have very different latency/safety tradeoffs:
| Commitment | Typical latency | Reorg risk |
|---|---|---|
processed |
~400ms after slot | High — leader's local state, can reorg |
confirmed |
~1.5-2s | Low — supermajority voted |
finalized |
~13-15s | None — irreversible |
Your subscription's commitment level controls when you receive notifications. If you subscribe with confirmed and the bot wants to react in real time, you're paying ~1.5s of latency on every event before you even see it.
// Fast but unsafe — sees state that may revert
wsConnection.onAccountChange(pool, callback, "processed");
// Slower but safe — sees state the network has agreed on
wsConnection.onAccountChange(pool, callback, "confirmed");
For a price-monitoring or arb bot, processed is usually right — you want maximum signal speed, and you're going to send a transaction anyway that the network will validate. For anything writing to permanent state (balances, settlements, accounting), use confirmed or higher.
The gotcha: if you set the connection-level commitment to confirmed and then pass processed to the subscription, the wsConnection.confirmTransaction() calls elsewhere will still use confirmed. Commitment is per-call. Be explicit.
Reconnect logic you cannot skip
Solana WebSocket connections drop. Not occasionally — routinely. Network blips, RPC node restarts, leader transitions, your own ISP. When a connection drops, the server forgets every one of your subscriptions. They don't transparently restore on reconnect.
@solana/web3.js v1 has partial automatic reconnect — it reopens the WebSocket but does NOT re-issue your subscription calls. You have to handle that yourself.
const subscribe = () => pools.map(pool =>
wsConnection.onAccountChange(pool, callback, "processed")
);
let subIds = subscribe();
// Detect disconnect and reset
wsConnection._rpcWebSocket.on("close", () => {
console.warn("WS closed, will reconnect");
});
wsConnection._rpcWebSocket.on("open", () => {
// Tear down stale handler IDs and resubscribe fresh
subIds.forEach(id => wsConnection.removeAccountChangeListener(id).catch(() => {}));
subIds = subscribe();
console.info("WS reconnected, resubscribed to", subIds.length, "accounts");
});
For production bots, also consider:
- Heartbeat: send a
getSlotcall every 30s to detect dead connections that haven't firedcloseyet - Backfill on reconnect: when the connection drops, you missed events. Do a one-shot
getMultipleAccountsInfoimmediately after resubscribing to catch up
When standard JSON-RPC isn't enough: gRPC and Geyser
Above ~1000 events per second across many accounts, even WebSocket subscriptions become a bottleneck — both because of per-subscription overhead and because some providers cap concurrent subscriptions per connection.
The next tier is Yellowstone gRPC (Geyser-based streaming). Instead of subscribing to individual accounts, you subscribe to a high-throughput stream of all account and transaction updates filtered server-side. Used by Jupiter, MEV bots, and large indexers.
It's not standard JSON-RPC — different protocol, different client, different pricing — but it's the right tool above a certain scale. If your bot is hitting WebSocket subscription limits, that's the door to consider.
For most bots, you're nowhere near needing it. WebSocket subscriptions to a handful of accounts at processed commitment will serve you well.
The takeaway
If your Solana bot polls for state changes, you're probably wasting 80%+ of your RPC budget and adding hundreds of milliseconds of unnecessary latency. Switch to accountSubscribe (or programSubscribe if you're watching a whole program), use processed commitment for read-only reaction logic, and write reconnect handlers — because Solana WebSocket connections will drop and your subscriptions will silently die otherwise.
HTTP isn't dead — use it for one-shot reads, simulations, and historical queries. But anything that runs in a polling loop with sub-second cadence should be a WebSocket subscription.
SwiftNodes provides Solana RPC over both HTTPS and WebSocket on every plan — same API key, no separate WebSocket endpoint to configure. See Solana RPC details or grab a free API key — no credit card, no KYC.
Related posts
- Surviving Solana RPC 429s: Rate Limits in Production
Solana's high block rate makes RPC rate limiting a constant production concern — 429s show up under load and break bots, indexers, and frontends. Here's why they happen, how to handle them with backoff and batching, and how to size your endpoint so they stop happening at all.
- How to Handle WebSocket Reconnections Without Losing Events
A WebSocket subscription that silently drops is worse than no subscription at all — you keep running, but events vanish into the gap. Here's how to build reconnect logic that detects the drop, backs off, re-subscribes, and backfills the missed events so your indexer never loses a log.
- Arbitrum WebSocket Gotchas We Hit in Production
Connecting to Arbitrum over WebSocket the same way you connect to Ethereum mainnet will bite you. The sequencer has no public mempool, newHeads is a firehose, and reconnecting isn't enough. Here's what actually breaks and how to handle it.