# WebSockets

> Subscriptions for accounts, logs, signatures and slots over a persistent WebSocket connection.

Source: https://docs.constant-k.com/rpc/websockets/

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

```text
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_KEY
```

The 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`](https://docs.constant-k.com/rpc/websocket/accountsubscribe/)           | An account's lamports or data change               |
| [`programSubscribe`](https://docs.constant-k.com/rpc/websocket/programsubscribe/)           | Any account owned by a program changes             |
| [`logsSubscribe`](https://docs.constant-k.com/rpc/websocket/logssubscribe/)                 | A transaction that mentions an address emits logs  |
| [`signatureSubscribe`](https://docs.constant-k.com/rpc/websocket/signaturesubscribe/)       | A transaction reaches the commitment you asked for |
| [`slotSubscribe`](https://docs.constant-k.com/rpc/websocket/slotsubscribe/)                 | The node processes a new slot                      |
| [`slotsUpdatesSubscribe`](https://docs.constant-k.com/rpc/websocket/slotsupdatessubscribe/) | A slot changes state inside the node               |
| [`rootSubscribe`](https://docs.constant-k.com/rpc/websocket/rootsubscribe/)                 | A new slot is rooted                               |
| [`voteSubscribe`](https://docs.constant-k.com/rpc/websocket/votesubscribe/)                 | The node sees a new vote                           |
| [`blockSubscribe`](https://docs.constant-k.com/rpc/websocket/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](https://docs.constant-k.com/rpc/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:

```json
{ "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

```bash
npx wscat -c "wss://ws-newyork.constant-k.com:1337/ws?api-key=YOUR_API_KEY"
```

A subscription:

```json
{ "jsonrpc": "2.0", "id": 1, "method": "slotSubscribe" }
```

The first reply is the subscription ID. Notifications follow:

```json
{ "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:

```json
{ "jsonrpc": "2.0", "id": 2, "method": "slotUnsubscribe", "params": [0] }
```

## Client examples

**TypeScript**

`@solana/kit` returns each subscription as an async iterator. Aborting the signal unsubscribes.

```ts title="account.ts"
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 unsubscribe
```

Logs that mention a program:

```ts title="logs.ts"
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");
}
```

**Python**

```bash
pip install websockets
```

This client subscribes to an account, sends a keepalive, and reconnects with backoff when the connection drops. Subscriptions do not survive a reconnect, so it subscribes again each time.

```python title="account.py"
import asyncio
import json
import os

import websockets

URL = f"wss://ws-newyork.constant-k.com:1337/ws?api-key={os.environ['CK_API_KEY']}"
USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"

async def keepalive(ws, every_s=30):
    # getHealth is accepted as a keepalive message
    while True:
        await asyncio.sleep(every_s)
        await ws.send(json.dumps({"jsonrpc": "2.0", "id": 0, "method": "getHealth"}))

async def stream_once():
    async with websockets.connect(URL) as ws:
        await ws.send(json.dumps({
            "jsonrpc": "2.0",
            "id": 1,
            "method": "accountSubscribe",
            "params": [USDC_MINT, {"encoding": "base64", "commitment": "confirmed"}],
        }))
        pinger = asyncio.create_task(keepalive(ws))
        try:
            async for raw in ws:
                msg = json.loads(raw)
                if msg.get("id") == 1:
                    print("subscription id:", msg["result"])
                elif msg.get("method") == "accountNotification":
                    result = msg["params"]["result"]
                    print(result["context"]["slot"], result["value"]["lamports"])
        finally:
            pinger.cancel()

async def main():
    delay = 1
    while True:
        try:
            await stream_once()
            delay = 1
        except (websockets.ConnectionClosed, OSError) as err:
            print("disconnected:", err)
        await asyncio.sleep(delay)
        delay = min(delay * 2, 30)  # back off, then reconnect and resubscribe

asyncio.run(main())
```

**web3.js v1**

`@solana/web3.js` v1 builds its WebSocket URL from the HTTP URL. constant-k uses separate WebSocket hostnames, so `wsEndpoint` has to be set explicitly:

```ts
import { Connection, PublicKey } from "@solana/web3.js";

const key = process.env.CK_API_KEY;

const connection = new Connection(`https://rpc-newyork.constant-k.com/?api-key=${key}`, {
  commitment: "confirmed",
  wsEndpoint: `wss://ws-newyork.constant-k.com:1337/ws?api-key=${key}`,
});

const subscriptionId = connection.onAccountChange(
  new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
  (accountInfo, context) => {
    console.log(`slot ${context.slot}: ${accountInfo.lamports} lamports`);
  },
  { commitment: "confirmed" },
);

// Later: await connection.removeAccountChangeListener(subscriptionId);
```

## Connection handling

* A socket with no other traffic needs `getHealth` every 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](#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](https://docs.constant-k.com/kaldera/) covers high-volume streams, broad program-wide filters and lower latency. It adds richer filters, compression, [replay](https://docs.constant-k.com/kaldera/replay/) after disconnects, and a dedicated lane per stream.
