Solana streaming

Solana WebSocket streams: subscriptions, heartbeats, and gap recovery

A production guide to consuming Solana data over WebSocket: how to subscribe and unsubscribe, respond to heartbeats, handle error frames and close codes, and recover missed events with slot-based backfill.

5 min readAnton Gilborn

Consuming Solana data over WebSocket is straightforward to start and subtle to get right in production. This guide covers the full Anaxer stream protocol: subscribing and unsubscribing, responding to heartbeats, handling error frames and close codes, and recovering missed events with slot-based backfill.

Who is this guide for?

This is for developers consuming any real-time Solana data over WebSocket and taking it to production:

  • Trading bots and execution systems that cannot miss events
  • Dashboards and monitoring tools that must stay live for hours
  • Data pipelines and indexers backfilling gaps after downtime
  • Alerting systems where a dropped connection means a missed alert

The protocol here is Anaxer's, but the patterns - heartbeats, backoff reconnects, slot-based gap recovery - apply to almost any Solana streaming source. If you just want events flowing in minutes, the SDKs handle all of this for you.

How do I subscribe and unsubscribe?

Connect to the stream endpoint, then send a subscribe message for the channel you want. Some channels, like trades, accept filters. Each connection handles one channel — open a separate connection for each stream you need.

ws.send(JSON.stringify({ subscribe: "trades", filters: { exchange: "pumpfun", side: "buy" } }));
// -> { "type": "subscribed", "channel": "trades" }

ws.send(JSON.stringify({ unsubscribe: "trades" }));
// -> { "type": "unsubscribed", "channel": "trades" }

Subscribe and unsubscribe control messages are limited to 10 per second per connection.

How do heartbeats keep the connection alive?

The server sends a heartbeat so both sides can detect a dead connection quickly.

ws.addEventListener("message", (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === "ping") {
    ws.send(JSON.stringify({ type: "pong" }));
    return;
  }
  // ... handle data events
});

If you do not reply to a ping within 10 seconds, the server closes the connection. This is intentional: a stalled client should fail fast and reconnect rather than silently miss data.

How should I handle errors and close codes?

Non-fatal problems arrive as inline error frames; fatal problems close the socket with an application code.

{ "type": "error", "code": "forbidden", "channel": "trades", "message": "Your plan doesn't include this stream." }

Key close codes:

CodeMeaning
1000Normal closure
4001Invalid or missing API key
4002Rate limit exceeded
4003Concurrent connection limit exceeded

Treat 4001 as fatal

A 4001 close means the key is wrong - reconnecting will not help. Surface it to the operator instead of looping. Codes like 4002 and 4003 should back off and retry.

How do I recover missed events after a reconnect?

This is the pattern that separates a toy from a production consumer. Track the last slot you processed. After reconnecting, backfill the gap with the matching REST endpoint before resuming the live feed.

let lastSlot = 0;

function onEvent(msg) {
  lastSlot = Math.max(lastSlot, msg.slot);
  process(msg);
}

async function backfill() {
  const res = await fetch(
    `https://api.anaxer.com/v1/trades?since=${lastSlot}&limit=100`,
    { headers: { Authorization: "Bearer YOUR_KEY" } }
  );
  const { data } = await res.json();
  for (const event of data) onEvent(event);
}

Because streams and REST share one event model, the same onEvent handler works for both live and backfilled data. See the streams overview for the complete protocol.

How do I reconnect without hammering the server?

When a connection drops, do not reconnect in a tight loop - that stampedes the server and can trip rate limits. Use exponential backoff with jitter, backfill the gap once the socket reopens, then resume the live feed.

function connect() {
  let attempt = 0;

  const open = () => {
    const ws = new WebSocket("wss://api.anaxer.com/v1/stream?key=YOUR_KEY");

    ws.addEventListener("open", () => {
      attempt = 0; // reset backoff on a healthy connection
      ws.send(JSON.stringify({ subscribe: "trades" }));
      backfill(); // catch up on anything missed while disconnected
    });

    ws.addEventListener("message", (event) => {
      const msg = JSON.parse(event.data);
      if (msg.type === "ping") return ws.send(JSON.stringify({ type: "pong" }));
      onEvent(msg);
    });

    ws.addEventListener("close", (event) => {
      if (event.code === 4001) return; // fatal: bad key, do not retry
      const base = Math.min(30_000, 1000 * 2 ** attempt++); // cap at 30s
      const delay = base / 2 + Math.random() * (base / 2); // add jitter
      setTimeout(open, delay);
    });
  };

  open();
}

The two details that matter: reset the attempt counter once a connection succeeds, and add jitter so many clients do not reconnect in lockstep after a shared outage.

Should I build this myself or use an SDK?

If you want to move fast, the official SDKs implement auth, heartbeats, exponential-backoff reconnects, gap recovery, and typed events, so you consume a clean event iterator. If you prefer control, the protocol above is small enough to implement directly.

Troubleshooting: common streaming problems

Most production issues trace back to one of these:

  • Connection closes after ~10 seconds of silence. You are not answering ping frames. Reply with pong inside the message handler, before any slow processing.
  • The socket reconnects constantly. Check the close code first - 4001 means a bad key (fatal, stop retrying); 4002/4003 mean back off and retry. Looping on a fatal code just wastes connections.
  • Events arrive out of order or duplicated. Order by slot, not arrival time or timestamp, and deduplicate on reconnect using the last slot you processed.
  • You miss events during deploys or blips. You are not backfilling. Store lastSlot, and on every reconnect call the REST endpoint with since=lastSlot before trusting the live feed.
  • Rate-limited (4002) on reconnect storms. Add jitter to your backoff so clients do not reconnect in lockstep, and keep subscribe/unsubscribe under 10 messages per second.
  • The handler falls behind under load. Do not do heavy work inline. Push events onto a queue and process off the socket thread so a slow consumer never stalls heartbeats.

Next steps

Apply this to a real feed: start with pump.fun launches or graduations. For limits per plan, see the rate limits reference and the pricing page.

Frequently asked questions

How do WebSocket heartbeats work on Anaxer?

The server sends a ping frame roughly every 30 seconds. Your client must reply with a pong within 10 seconds, or the connection is closed. The official SDKs handle this automatically.

How do I recover events missed during a disconnect?

Track the last slot you processed. After reconnecting, call the matching REST endpoint with since set to that slot to backfill the gap, then resume the live stream.

Can one connection subscribe to multiple channels?

No. Each WebSocket connection is one stream, subscribed to a single channel. Your plan sets how many concurrent streams you can run — Free: 1, Starter: 4, Pro: 8. To follow creations and graduations at once, open two connections.

What is a slot in Solana and why does it matter for streaming?

A slot is Solana's unit of time - a window in which a leader can produce a block, roughly every 400ms. Every Anaxer event carries the slot it occurred in, which gives you a strict on-chain ordering that timestamps cannot. Order events by slot, and use the last slot you processed as the cursor for REST backfill after a reconnect.

Why does the server close my WebSocket connection?

The most common cause is a missed heartbeat: if you do not reply to a ping with a pong within 10 seconds, the server closes the connection so a stalled client fails fast. Other closures use application codes - 4001 (invalid key, fatal), 4002 (rate limit), and 4003 (concurrent stream limit exceeded).

How should I reconnect a dropped Solana WebSocket?

Use exponential backoff with jitter, capped at around 30 seconds, and reset the delay once a connection succeeds. Do not retry on a fatal 4001 (bad key). On every successful reconnect, backfill missed events via REST using since set to your last processed slot before resuming the live feed.

Last updated June 20, 2026.

Start streaming Solana data

Anaxer gives you real-time pump.fun and PumpSwap streams and REST APIs on a flat plan. The free tier needs no card.

Written by Anton Gilborn · Senior Platform Engineer, Anaxer

Anton is a senior platform engineer at Anaxer, where he builds the real-time streaming infrastructure behind its Solana data API - token launches, graduations, DEX trades, and prices. He writes about low-latency streaming architecture and building fast on-chain apps.

Keep reading