Solana WebSocket Subscriptions: onLogs and the Gotchas

How to subscribe to Solana accounts and program logs without missing events: commitment levels, reconnection, and why a subscription is not verification.

BoltTx Team··9 min read
solanawebsocketonlogssubscriptionstrading-botrpc

Polling an RPC on a timer works until you need to know about something the moment it happens. Then you switch to WebSocket subscriptions, and discover a different set of problems.

Subscriptions are faster. They are also silently lossy in ways polling is not, and most bugs in subscription code come from treating a push as a guarantee.

The Four Subscriptions Worth Knowing

Solana exposes several, but four cover almost every real use case:

onAccountChange (the accountSubscribe RPC method) — fires when a specific account's data changes. Best for watching a pool, a curve, or a token account.

onProgramAccountChange (the programSubscribe RPC method) — fires for any account owned by a program, optionally filtered. Powerful and easy to make expensive.

onLogs — fires when a program or address appears in transaction logs. This is the one most trading bots use, because it catches activity you cannot predict the account for.

onSignature — fires once when a specific signature reaches a commitment level. Useful for confirmation, not for discovery.

Commitment Level Is the First Decision

Every subscription takes a commitment level, and the choice is a real trade-off rather than a default to accept.

connection.onLogs(
  programId,
  (logs) => handle(logs),
  "processed", // vs "confirmed" vs "finalized"
);
Level Speed Risk
processed ★fastest★ may be rolled back
confirmed one slot or more behind rarely rolled back
finalized many slots behind effectively permanent

★For anything time-sensitive, use processed and verify before acting on money.★ Waiting for confirmed means waiting for the thing you are racing. You accept that a small fraction of what you see may not survive, and you handle that with verification rather than delay.

For anything that writes to your own database as truth, use confirmed or better.

If you already have subscriptions working and the problem is on the send side, a free BoltTx key is one line to test against.

The Reconnection Problem

WebSocket connections drop. Not occasionally — routinely, from network blips, provider restarts, and idle timeouts.

The default client behaviour is not what most people assume:

// This looks fine and silently stops working after a drop.
connection.onLogs(programId, handler, "processed");

★When the socket dies, your handler stops being called. No error, no exception — just silence.★ A bot can run for hours receiving nothing while looking perfectly healthy.

The fix is a liveness check that does not depend on events arriving, since "no events" is indistinguishable from "connection dead" when the thing you watch is quiet:

let lastEventAt = Date.now();
let subId: number | null = null;

async function subscribe() {
  subId = connection.onLogs(
    programId,
    (logs) => {
      lastEventAt = Date.now();
      handle(logs);
    },
    "processed",
  );
}

// Independent heartbeat: prove the connection is alive using a
// call whose result you can predict, rather than waiting for events.
setInterval(async () => {
  try {
    await connection.getSlot("processed");   // fails if the socket is gone
    if (Date.now() - lastEventAt > 120_000) {
      // Quiet for two minutes on a program that should be busy.
      // Resubscribe rather than assume the network is idle.
      if (subId !== null) await connection.removeOnLogsListener(subId);
      await subscribe();
    }
  } catch {
    if (subId !== null) await connection.removeOnLogsListener(subId).catch(() => {});
    await subscribe();
  }
}, 30_000);

Logs Are Not Data

onLogs gives you log lines and a signature. It does not give you authoritative state.

connection.onLogs(programId, async (logs) => {
  // Wrong: parsing amounts out of log strings.
  // Log formats are not a stable interface and change between versions.
  const amount = parseAmountFromLogs(logs.logs);

  // Right: use the log as a trigger, then read state.
  const account = await connection.getAccountInfo(derivedPda);
  const state = deserialize(account.data);
});

★Treat the subscription as a notification, not as the payload.★ The log tells you something happened; the account tells you what is true.

This matters most when the log format changes — which happens on program upgrades, and breaks parsers silently rather than loudly.

Where Subscriptions Get Expensive

onProgramAccountChange without filters on a busy program will flood you. Every account owned by that program, every change, pushed to your process.

// Expensive: every account under the program.
connection.onProgramAccountChange(programId, handler);

// Better: filter server-side so you only receive what you need.
connection.onProgramAccountChange(
  programId,
  handler,
  "processed",
  [
    { dataSize: 165 },                                  // only this layout
    { memcmp: { offset: 32, bytes: ownerPubkey.toBase58() } },
  ],
);

★Filters run on the provider's side, so an unfiltered subscription costs bandwidth and CPU on both ends.★ On a shared tier it is also a fast way to hit limits.

Subscriptions Do Not Help Sending

Worth stating plainly, because it is a common assumption.

A WebSocket subscription makes you learn about events sooner. It does nothing for how quickly your own transaction reaches a block producer, because submission is an HTTP call on a different path.

detection    → WebSocket subscription helps here
submission   → ★unaffected by your subscription setup★

This is why many production setups use one provider for streaming and a delivery-focused endpoint for sends. The two workloads want different things, and the integration points are independent.

★A bot with the fastest possible detection and a slow submission path still arrives late.★

What Landing Looks Like

Real transactions through our delivery nodes: median confirmation 336ms — under one slot.

If your detection is instant but you consistently land four or more slots after the event you reacted to, the gap is on the send side rather than in your subscription code.

Where BoltTx Fits

We do not do subscriptions. We do submission.

BoltTx routes signed transactions 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. Pair us with whatever streaming provider fits your detection needs — the two integration points are independent.

You sign locally. We never hold funds, never sign, and never modify transaction contents. 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:

// Detection stays where it is; only the send endpoint changes.
const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

FAQ

What is a Solana WebSocket subscription? A persistent connection where the RPC pushes events to you instead of you polling for them. The common ones are onAccountChange, onProgramAccountChange, onLogs, and onSignature.

What commitment level should I use for subscriptions? processed for time-sensitive detection, since waiting for confirmed costs a slot or more. Use confirmed or finalized for anything you record as truth, because processed results can be rolled back.

Why did my Solana WebSocket subscription stop firing? The connection dropped and your handler was never called again. There is no error thrown, so the failure is silent. Add an independent heartbeat that verifies liveness rather than inferring it from events arriving.

How do I detect that a Solana WebSocket has disconnected? Do not rely on event silence, since a quiet program looks identical to a dead socket. Call something with a predictable result on a timer and resubscribe when it fails or when you have been quiet longer than the program plausibly would be.

Is onLogs reliable enough to trade on? As a trigger, yes. As a data source, no. Use the log to learn that something happened, then read the account for authoritative state. Log formats change on program upgrades and break parsers silently.

What is the difference between onLogs and onProgramAccountChange? onLogs fires when a program appears in transaction logs, which catches activity involving accounts you could not predict. onProgramAccountChange fires when accounts owned by a program change, which is more precise but requires knowing what to watch.

How do I avoid getting flooded by onProgramAccountChange? Apply filters — dataSize and memcmp — so the provider only sends matching accounts. An unfiltered subscription on a busy program pushes everything, which costs bandwidth on both sides and burns rate limits.

Do WebSocket subscriptions count against my rate limit? Usually differently from HTTP requests, and provider policies vary. What is consistent is that unfiltered high-volume subscriptions are the fastest way to hit whatever limit exists.

Can I use WebSocket subscriptions to send transactions? Sending is an HTTP call, not a subscription. Some clients open a WebSocket for confirmation notifications, but the submission itself takes a different path — which is why detection speed and submission speed are separate problems.

Why does my bot detect events instantly but still trade late? Because detection and submission are independent. A subscription tells you sooner; it does nothing for how fast your transaction reaches a block producer. Log the slot where you detected and the slot where you landed to see which half is costing you.

How many subscriptions can I have on one connection? Practically, more than most bots need, though providers set their own caps. The constraint is usually the volume of events flowing through rather than the number of subscriptions open.

Should I use one provider for subscriptions and another for sending? Many production setups do. Streaming wants throughput and broad filtering; sending wants a short path to a block producer. Optimising one endpoint for both means compromising on each, and the integration points are independent.

What happens to events while my WebSocket is reconnecting? They are lost. There is no replay buffer. If missing events is unacceptable, backfill with a polling query covering the gap once you reconnect.

Is onSignature useful for a trading bot? For confirming a transaction you sent, yes — it fires once and then unsubscribes. For discovering new activity, no, because you have to know the signature in advance.

How do I test that my reconnection logic works? Kill the network on the machine running the bot for thirty seconds and verify you resubscribe and resume. Waiting for a real drop to test it means finding out during a period you cared about.

Back to all posts