Solana WebSocket vs HTTP RPC for 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.
John Sullivan covers RPC infrastructure, node operations, and multi-chain development at SwiftNodes — what it actually takes to keep endpoints fast, fresh, and reliable across EVM and non-EVM networks.
Related posts
- How Old Is 'finalized'? We Measured 62 Chains
eth_getBlockByNumber('finalized') ranges from instant to four hours across 62 EVM chains; five give no answer, one returns genesis. The table and code.
- trace_filter and trace_transaction: Beyond Receipts
Receipts show that a transaction succeeded; traces show what it did. How trace_transaction and trace_filter expose internal calls, where the Parity trace namespace actually works in 2026 (we probe it weekly — it's 11 of 54 EVM chains), and the param quirks that trip people up.
- JSON-RPC Error Codes: Retry, Fix, or Accept?
-32601, -32000, code 3, HTTP 429 — every RPC error belongs to one of three families: try another node, fix your request, or accept the chain's answer. A field guide with real error payloads.