Skip to content
A held connectionA WebSocket stays open: transactions go out and results come back over the same connection.SENDRESULT
A WebSocket stays open: transactions go out and results come back over the same connection.

Sending via WebSocket

Persistent wss:// connection — open once, stream as many transactions or bundles as you need. The TLS handshake is paid once at connect time and amortized across every subsequent message. Browser-native, language-agnostic, identical validation and tip rules to HTTPS.

Endpoints

StreamURL pattern
Single transactionswss://{region}.fastrelay.sh/v1/stream/tx
Atomic bundles (≤ 5 txs)wss://{region}.fastrelay.sh/v1/stream/bundle

{region} is fra, ams, ny, or tyo. The bare fastrelay.sh is the website, not a relay.

Authentication

Same as HTTPS — auth happens once at the upgrade request, not per message. Choose either form:

js
// Header (preferred for non-browser clients)
new WebSocket("wss://fra.fastrelay.sh/v1/stream/tx", undefined, {
  headers: { "x-api-key": "YOUR_API_KEY" }
});

// Query parameter (works from browsers, where headers can't be set on WS)
new WebSocket("wss://fra.fastrelay.sh/v1/stream/tx?api-key=YOUR_API_KEY");

API keys are optional. Without one, public submissions still work; with one, you get rate tracking and the stake-scaled rate limit.

Single-Transaction Stream

Each frame you send is one JSON object:

json
{ "id": "client-correlation-id", "tx": "BASE64_ENCODED_TRANSACTION" }

The server replies with a matching frame:

json
{ "id": "client-correlation-id", "status": "accepted", "request_id": "uuid", "signature": "5xy…" }

On rejection:

json
{ "id": "client-correlation-id", "status": "rejected", "error": "Tip 100000 below threshold 1000000" }

The id field is yours — it's echoed back verbatim so you can match each reply to the request that produced it. Replies may arrive out of order; correlate by id.

Bundle Stream

Atomic groups of up to 5 transactions that land together or not at all:

json
{ "id": "b1", "txs": ["BASE64_TX_1", "BASE64_TX_2", "BASE64_TX_3"] }

Server reply:

json
{ "id": "b1", "status": "accepted", "bundle_id": "jito-uuid", "signatures": ["sig1", "sig2", "sig3"] }

bundle_id is the Jito Block Engine's bundle id — you can track the bundle with it directly. Tip convention: any single transaction paying the standard 0.001 SOL FastRelay tip covers the whole group, and the bundle must additionally include a Jito tip. Bundles are submitted only through the Jito Block Engine's atomic path — never split across TPU/RPC channels — so atomicity is preserved end-to-end.

Pipelined Sends

You don't have to wait for a reply before sending the next message. Fire as many frames as your rate limit allows; replies stream back asynchronously, identified by id.

js
for (const tx of myTxs) {
  ws.send(JSON.stringify({ id: tx.localId, tx: tx.b64 }));
}
// Replies arrive out of order — match by id.

A rate-limit rejection on one frame doesn't close the connection — the next frame goes through normally.

Browser Example

js
const ws = new WebSocket(
  "wss://fra.fastrelay.sh/v1/stream/tx?api-key=YOUR_API_KEY"
);

ws.onopen = () => {
  ws.send(JSON.stringify({ id: "1", tx: BASE64_TX }));
};

ws.onmessage = (e) => {
  const r = JSON.parse(e.data);
  if (r.status === "accepted") {
    console.log("landed", r.signature);
  } else {
    console.error("rejected", r.error);
  }
};

Node.js Example (ws)

js
import WebSocket from "ws";

const ws = new WebSocket("wss://fra.fastrelay.sh/v1/stream/tx", {
  headers: { "x-api-key": "YOUR_API_KEY" },
});

ws.on("open", () => {
  ws.send(JSON.stringify({ id: "1", tx: BASE64_TX }));
});

ws.on("message", (data) => {
  const r = JSON.parse(data.toString());
  console.log(r);
});

Python Example (websockets)

python
import asyncio, json, websockets

async def submit(tx_b64_list):
    async with websockets.connect(
        "wss://fra.fastrelay.sh/v1/stream/tx",
        additional_headers={"x-api-key": "YOUR_API_KEY"},
    ) as ws:
        # Pipeline: fire all txs, collect replies as they arrive
        for i, tx in enumerate(tx_b64_list):
            await ws.send(json.dumps({"id": str(i), "tx": tx}))
        for _ in tx_b64_list:
            print(json.loads(await ws.recv()))

Rust Example (tokio-tungstenite)

rust
use futures_util::{SinkExt, StreamExt};
use tokio_tungstenite::{connect_async, tungstenite::Message};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let url = "wss://fra.fastrelay.sh/v1/stream/tx?api-key=YOUR_API_KEY";
    let (mut ws, _) = connect_async(url).await?;

    let frame = serde_json::json!({ "id": "1", "tx": tx_base64 });
    ws.send(Message::Text(frame.to_string())).await?;

    while let Some(msg) = ws.next().await {
        if let Message::Text(text) = msg? {
            println!("{}", text);
            break;
        }
    }
    Ok(())
}

Connection Lifecycle

  • Idle behavior: streams stay alive aggressively — no idle timeout you need to design around. A connection opened at process boot will stay hot through quiet periods.
  • Reconnect: on transient drops (network blip, server restart), reconnect immediately and resume sending. There's no session state to recover — each frame is independent.
  • Close: send a close frame when you're done; the server will flush any in-flight replies and finish the close handshake.

Important: Tip and validation rules are identical to HTTPS and QUIC — minimum 0.001 SOL to a registered tip wallet, no transactions using Address Lookup Tables (ALT), max ~1.6KB per transaction. Bundles inherit the same threshold (one tx paying the FastRelay tip covers the whole bundle) and additionally require a Jito tip.