Solana WebSocket vs HTTP RPC for Bots

By John Sullivan · May 30, 2026 · 6 min read · #solana #websocket #json-rpc #tutorial

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. simulateTransaction is request-response by nature.
  • Reading historical state. Subscriptions are forward-looking only; for getAccountInfo at a specific slot or getTransaction of an old signature, you need HTTP.
  • Batched read across many accounts you check rarely. getMultipleAccountsInfo for 100 accounts every 10 seconds is more efficient as a single batched HTTP call than 100 long-lived subscriptions.
  • Confirming a transaction was successful. signatureSubscribe exists, but a single getSignatureStatuses call 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 getSlot call every 30s to detect dead connections that haven't fired close yet
  • Backfill on reconnect: when the connection drops, you missed events. Do a one-shot getMultipleAccountsInfo immediately 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.

J
John Sullivan
Infrastructure Writer, SwiftNodes

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.

Try SwiftNodes free — multi-chain RPC across 83 networks, flat-rate pricing, pay by card or crypto, no KYC. Get an API key in 30 seconds →