Solana RPC: WebSocket vs HTTP for High-Frequency Bots

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.

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.

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