Solana streaming

Solana WebSocket streams: subscriptions, keepalives, and gap recovery

A production guide to consuming Solana data over WebSocket: how to subscribe and unsubscribe, optional client pings, handle error frames and close codes, and recover missed events with from/to REST backfill.

7 min readAnton Gilborn

Consuming Solana data over WebSocket is straightforward to start and subtle to get right in production. This guide covers the Anaxer stream protocol as implemented on the live gateway: subscribing and unsubscribing, optional client keepalives, handling error frames and close codes, and recovering missed events with timestamp-based REST backfill.

For the landing-page version of this guide — connect URL, subscribe frames, and production checklist in one place — see the Pump.fun WebSocket API.

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 - backoff reconnects, slot-based ordering, REST gap recovery - apply to almost any Solana streaming source. If you just want events flowing in minutes, the SDKs handle auth and reconnects for you (live resume only in v1 — REST backfill for gaps is still on you).

How do I subscribe and unsubscribe?

Connect to wss://api.anaxer.com/v1/stream?apiKey=YOUR_KEY (or Bearer on the upgrade). After the handshake the server sends a connected frame with your plan limits. Then send a subscribe message for each channel you want. Channels accept optional filters — for example trades supports sources and USD volume bounds; creations supports enriched and excludeMayhem; graduations supports excludeMayhem and minLiquiditySol. One connection may hold multiple channel subscriptions, subject to the account-wide per-channel cap.

ws.send(JSON.stringify({
  type: "subscribe",
  id: "trades-1",
  channel: "trades",
  filters: { sources: ["pump_fun"], minVolumeUsd: 100 },
}));
// -> { "v": 1, "type": "subscribed", "id": "trades-1", "channel": "trades", "filters": { … }, "ts": … }

ws.send(JSON.stringify({
  type: "subscribe",
  id: "launches",
  channel: "creations",
  filters: { enriched: true, excludeMayhem: true }
}));

ws.send(JSON.stringify({
  type: "subscribe",
  id: "grads",
  channel: "graduations",
  filters: { excludeMayhem: true, minLiquiditySol: 50 }
}));

ws.send(JSON.stringify({ type: "unsubscribe", id: "trades-1" }));
// -> { "v": 1, "type": "unsubscribed", "id": "trades-1", "ts": … }

Note that enriched is WebSocket-only — REST creations do not take that query param. Exceeding the per-channel subscription cap returns subscription_limit.

How do keepalives work?

Client pings are optional. Send one when you want to confirm the socket is alive; the server answers with a versioned pong.

ws.send(JSON.stringify({ type: "ping" }));
// -> { "v": 1, "type": "pong", "ts": … }

The server does not push application-level pings on a schedule. It may still close idle or dead sockets. Prefer detecting stalls in your own client (or use the TypeScript SDK, which reconnects for you).

How should I handle errors and close codes?

Non-fatal problems arrive as inline versioned error frames; fatal problems send an error frame then close the socket.

{
  "v": 1,
  "type": "error",
  "code": "plan_restricted",
  "message": "Your plan doesn't include this stream.",
  "id": "trades-1",
  "ts": 1783259497400
}

Common codes: invalid_message, invalid_filters, unknown_channel, subscription_limit, duplicate_id, unknown_subscription, plan_restricted, internal_error (connection stays open). Fatal: unauthorized → close 1008; slow_consumer → close 1013. Unexpected failures may close with 1011.

Treat 1008 as fatal

A 1008 close after unauthorized means the key is wrong or revoked - reconnecting will not help. Surface it to the operator instead of looping. After 1013 (slow consumer), back off, drain work off the socket thread, then reconnect.

How do I recover missed events after a reconnect?

This is the pattern that separates a toy from a production consumer. Track the last event timestamp (unix ms) and slot. After reconnecting, backfill the gap with the matching REST list endpoint using from / to and walk next cursors — there is no REST since parameter.

let lastTs = 0;
let lastSlot = 0;

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

async function backfill() {
  const res = await fetch(
    `https://api.anaxer.com/v1/tokens/EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm/trades?from=${lastTs || "<lastEventTimestamp>"}&limit=100`,
    { headers: { Authorization: "Bearer YOUR_KEY" } }
  );
  const { data, next } = await res.json();
  for (const event of data) onEvent(event);
  // walk `next` cursors until exhausted
}

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. 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?apiKey=YOUR_KEY");

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

    ws.addEventListener("message", (event) => {
      const msg = JSON.parse(event.data);
      if (msg.type === "pong" || msg.type === "subscribed" || msg.type === "connected") return;
      if (msg.type === "error") {
        console.error(msg.code, msg.message);
        return;
      }
      if (msg.channel === "trades") onEvent(msg.data);
    });

    ws.addEventListener("close", (event) => {
      if (event.code === 1008) return; // fatal: unauthorized, 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 TypeScript SDK implements auth, exponential-backoff reconnects, and typed events, so you consume a clean event stream. Gap backfill after a disconnect is still a REST concern in v1 — the SDK resumes the live feed only. If you prefer control, the protocol above is small enough to implement directly. Python works with any standard WebSocket library (an official Python SDK is planned).

Troubleshooting: common streaming problems

Most production issues trace back to one of these:

  • No events after connecting. Wait for connected, then send subscribe inside the open handler, and confirm you received subscribed before expecting data frames.
  • The socket reconnects constantly. Check the close code first - 1008 means unauthorized (fatal, stop retrying); 1013 means you were a slow consumer (back off, process off-thread, retry).
  • Events arrive out of order or duplicated. Order by slot, not arrival time, and deduplicate on reconnect using the last slot you processed.
  • You miss events during deploys or blips. You are not backfilling. Store lastTs, and on every reconnect call the REST endpoint with from=lastTs (walk next) before trusting the live feed.
  • subscription_limit on subscribe. You already hold the max concurrent subscriptions for that channel on this connection (or across connections — see plan caps). Unsubscribe an idle sub or upgrade.
  • 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 triggers 1013.

Next steps

Apply this to a real feed: start with pump.fun launches or graduations. If you are weighing a raw Yellowstone gRPC firehose against a purpose-built parsed stream, read raw gRPC or parsed events. For limits per plan, see the rate limits reference and the pricing page.

Frequently asked questions

How do WebSocket keepalives work on Anaxer?

Client ping frames are optional in v1. Send { type: ping } when you want a liveness check; the server replies with a versioned pong. The server does not send application-level pings on a timer. The official TypeScript SDK handles reconnection for you.

How do I recover events missed during a disconnect?

Track the last event timestamp (and slot for ordering). After reconnecting, call the matching REST list endpoint with from/to (and walk next cursors) to backfill the gap, then resume the live stream. There is no REST since parameter.

Can one connection subscribe to multiple channels?

Yes. One WebSocket connection may hold subscriptions on multiple channels. Your plan caps concurrent subscriptions per channel across your account — Free: 1, Starter: 1, Pro: 2 — and every connection shares that counter. Exceeding the cap returns subscription_limit.

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 event timestamp as from for REST backfill after a reconnect.

Why does the server close my WebSocket connection?

Fatal stream errors send an error frame then close the socket: unauthorized closes with 1008 (bad or missing key — do not retry), slow_consumer closes with 1013 (you fell behind; back off), and unexpected failures may close with 1011. Most other error codes leave the connection open.

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 after a 1008 unauthorized close. On every successful reconnect, re-subscribe and backfill missed events via REST using from set to your last event timestamp before trusting the live feed.

Last updated July 25, 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, prices, and transfers. He writes about low-latency streaming architecture and building fast on-chain apps.

Keep reading