WebSockets
Subscriptions for accounts, logs, signatures and slots over a persistent WebSocket connection.
The WebSocket endpoint speaks the standard Solana PubSub protocol: one connection carries subscriptions, and the node pushes a notification on each change. It is available from the Operator plan up.
Endpoints
wss://ws-newyork.constant-k.com:1337/ws?api-key=YOUR_API_KEY
wss://ws-frankfurt.constant-k.com:1337/ws?api-key=YOUR_API_KEYThe port is 1337 and the path is /ws. The key goes in the query string.
Plan limits
| Operator | Blade Runner | Nexus | |
|---|---|---|---|
| Concurrent connections per key | 10 | 50 | 100 |
| Subscriptions per connection | 10 | 50 | 100 |
| New connections per key | 10 per 60 seconds | 10 per 60 seconds | 10 per 60 seconds |
| Requests per second (shared with HTTP RPC) | 50 | 200 | 500 |
blockSubscribe | — | — | ✓ |
The Free plan does not include WebSockets. Connection and subscription counts are held by each region separately.
Both caps apply at once: a Nexus key can hold 100 connections, each carrying up to 100 subscriptions.
| Cap reached | Result |
|---|---|
| Subscriptions per connection | The subscribe call returns an error frame with code -32005 and the message Maximum subscriptions reached for your tier. The existing connection stays open. |
| Connections per key, or new connections per 60 seconds | The new connection is closed with code 1013 and a reason saying which. |
Connection pacing applies on every plan. On Operator, opening 10 connections takes a full minute, so a long-lived connection avoids paying that cost again.
Subscription methods
The socket accepts subscription methods only, plus getHealth. There is no pass-through: any other method returns an error frame. Other calls go over HTTPS.
| Subscribe | Notification when |
|---|---|
accountSubscribe | An account's lamports or data change |
programSubscribe | Any account owned by a program changes |
logsSubscribe | A transaction that mentions an address emits logs |
signatureSubscribe | A transaction reaches the commitment you asked for |
slotSubscribe | The node processes a new slot |
slotsUpdatesSubscribe | A slot changes state inside the node |
rootSubscribe | A new slot is rooted |
voteSubscribe | The node sees a new vote |
blockSubscribe | A block is confirmed or finalized (Nexus only) |
Each …Subscribe method has a matching …Unsubscribe method that takes the subscription ID. Unsubscribing does not count against your rate limit or your subscription cap.
blockSubscribe
blockSubscribe needs the Nexus plan, and its options object must set "maxSupportedTransactionVersion": 1 as the JSON number 1. Without it, or with the string "1", the subscribe call fails with -32602. A notification whose block is null is a failure, not an empty block. See Transaction v1.
Keepalives
getHealth is the keepalive this endpoint accepts as a message. WebSocket protocol ping frames are a different thing and are answered with a pong. Where a client offers both, the protocol ping frames are the better choice.
@solana/kit and @solana/web3.js v1 send a JSON-RPC message named ping roughly every 5 seconds. ping is not a subscription method, so each one comes back as an error frame:
{ "jsonrpc": "2.0", "error": { "code": -32600, "message": "ping is not allowed over WebSocket. Use the RPC endpoint for non-subscription calls." }, "id": null }The connection stays open, subscriptions keep delivering, and the message counts against nothing. An error handler that treats an unsolicited error as a broken socket has to ignore -32600 frames whose id is null.
Idle timeout
There is no short idle timeout. A connection is closed after 24 hours with no traffic in either direction. It is closed abruptly, with no close frame, so most clients report code 1006.
Any subscription that delivers, or a keepalive every 30 seconds, keeps a connection inside that window. Code 1006 means the connection has to be reopened and the subscriptions made again.
A wscat session
npx wscat -c "wss://ws-newyork.constant-k.com:1337/ws?api-key=YOUR_API_KEY"A subscription:
{ "jsonrpc": "2.0", "id": 1, "method": "slotSubscribe" }The first reply is the subscription ID. Notifications follow:
{ "jsonrpc": "2.0", "result": 0, "id": 1 }
{ "jsonrpc": "2.0", "method": "slotNotification", "params": { "result": { "parent": 447913192, "root": 447913161, "slot": 447913193 }, "subscription": 0 } }slotUnsubscribe with the ID stops it:
{ "jsonrpc": "2.0", "id": 2, "method": "slotUnsubscribe", "params": [0] }Client examples
@solana/kit returns each subscription as an async iterator. Aborting the signal unsubscribes.
import { address, createSolanaRpcSubscriptions } from "@solana/kit";
const rpcSubscriptions = createSolanaRpcSubscriptions(
`wss://ws-newyork.constant-k.com:1337/ws?api-key=${process.env.CK_API_KEY}`,
);
const abortController = new AbortController();
// Account changes for the USDC mint
const notifications = await rpcSubscriptions
.accountNotifications(address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), {
commitment: "confirmed",
encoding: "base64",
})
.subscribe({ abortSignal: abortController.signal });
for await (const { context, value } of notifications) {
console.log(`slot ${context.slot}: ${value.lamports} lamports`);
}
// Call abortController.abort() to unsubscribeLogs that mention a program:
import { address, createSolanaRpcSubscriptions } from "@solana/kit";
const rpcSubscriptions = createSolanaRpcSubscriptions(
`wss://ws-newyork.constant-k.com:1337/ws?api-key=${process.env.CK_API_KEY}`,
);
const abortController = new AbortController();
const logs = await rpcSubscriptions
.logsNotifications(
{ mentions: [address("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8")] },
{ commitment: "confirmed" },
)
.subscribe({ abortSignal: abortController.signal });
for await (const { value } of logs) {
if (value.err) continue;
console.log(value.signature, value.logs.length, "log lines");
}Connection handling
- A socket with no other traffic needs
getHealthevery 30 seconds or so. - A reconnect uses exponential backoff and then subscribes again. Subscription IDs belong to the old connection.
- An unused subscription keeps sending data and uses bandwidth on both ends until it is cancelled.
- One connection carries many subscriptions. More connections are only worth opening once the per-connection cap is reached.
- One region per flow. A subscription on one region and HTTP reads on another can disagree by a slot or two.
Troubleshooting
| Symptom | Cause |
|---|---|
The connection closes right after it opens, often code 1006 | The key is missing or invalid, or your plan does not include WebSockets. The handshake succeeds first, so a bad key looks like a network fault |
| An error frame arrives every few seconds, but data keeps flowing | Your client is sending its own ping message. See Keepalives |
The connection closes with code 1013 | You are at your connection limit, or over 10 new connections per minute. The close reason names which |
A new subscription returns -32005 | You are at the subscription cap for that connection, or over a rate limit. Another connection, or an unsubscribe, frees room |
@solana/web3.js subscriptions never fire | wsEndpoint is not set, so the client does not connect to the WebSocket endpoint |
blockSubscribe fails with -32602 | It needs the Nexus plan and "maxSupportedTransactionVersion": 1 as a number, not the string "1" |
A call such as getBalance fails over the socket | Only subscriptions and getHealth are allowed. Other calls go over HTTPS |
WebSockets and Kaldera
WebSockets suit apps that watch a handful of accounts or signatures. Kaldera gRPC covers high-volume streams, broad program-wide filters and lower latency. It adds richer filters, compression, replay after disconnects, and a dedicated lane per stream.