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.
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:
| Code | Meaning |
|---|---|
| 1000 | Normal closure |
| 4001 | Invalid or missing API key |
| 4002 | Rate limit exceeded |
| 4003 | Concurrent 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
pingframes. Reply withponginside the message handler, before any slow processing. - The socket reconnects constantly. Check the close code first -
4001means a bad key (fatal, stop retrying);4002/4003mean 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 withsince=lastSlotbefore 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
How to track pump.fun token launches in real time
Subscribe to the creations stream and get a clean JSON event the instant any pump.fun token is minted - here is the complete guide, from your first connection to production-grade gap recovery.
How to detect pump.fun graduations the moment they happen
A token graduating from pump.fun to PumpSwap is one of the highest-signal events in the ecosystem. Here is how to catch every graduation instantly, with liquidity data attached.
Anaxer vs Helius: which is better for pump.fun data?
Helius is an excellent general-purpose Solana RPC - but it meters every stream by data volume, and pump.fun is a firehose. Here is an honest, head-to-head comparison with Anaxer, a cost-effective flat-rate option for real-time pump.fun and PumpSwap data.