WebSocket Streaming Comes to AllenHark Relay: Persistent Connections for Transactions and Bundles
AllenHark Relay now supports persistent wss:// connections — open one socket, stream as many transactions or bundles as you want. Less handshake overhead, lower latency, browser-native, and bundle-aware.
WebSocket Streaming Comes to AllenHark Relay
We just shipped WebSocket support across all four AllenHark Relay regions. You can now open a single persistent connection to any region and stream transactions — or full atomic bundles — for as long as you like. No new handshake per request, no TCP slow-start every time, no hand-rolled QUIC client just to keep latency tight.
This post is about why we built it, what it changes, and how to use it.
The Three Submission Paths You Now Have
AllenHark Relay has always exposed two ways to submit a transaction:
- HTTPS at
https://{region}.relay.allenhark.com/v1/sendTx— one TCP+TLS handshake per transaction. Easy to integrate. Bad latency profile when you're sending continuously. - QUIC at
{ip}:4433— persistent connection with multiplexed streams. The leanest path in production, but a real Rust/Go client is essentially required, and browsers can't speak it.
WebSockets fill the gap. As of today:
| Protocol | Endpoint | Best for |
|---|---|---|
| HTTPS | https://{region}.relay.allenhark.com/v1/sendTx | Simple integrations, infrequent sends |
| WSS (tx) | wss://{region}.relay.allenhark.com/v1/stream/tx | Continuous streaming, browsers, language-agnostic |
| WSS (bundle) | wss://{region}.relay.allenhark.com/v1/stream/bundle | Atomic multi-tx bundles |
| QUIC | {ip}:4433 | Leanest transport, native Rust/Go clients |
{region} is fra, ams, ny, or tyo. relay.allenhark.com is a synonym for Frankfurt.
Why a Persistent Socket Matters
A new HTTPS request to a Solana relay isn't free. You're paying:
- TCP handshake — one round-trip
- TLS handshake — one or two more round-trips
- HTTP request framing — headers parsed every time
- Connection close — fin, ack, retransmit windows discarded
That's several round trips before your first byte of transaction data even gets read, and more on a bad link or a cold connection. For a sniper bot or arbitrage system that's submitting hundreds of transactions a minute, that handshake tax compounds into real money.
A WebSocket connection pays the handshake cost once. After that, every transaction is just a JSON frame on a hot, kept-alive socket. Round-trip times drop to whatever the underlying network gives you.
It's the same insight that makes our QUIC endpoint fast. WebSockets just bring it to anyone who can speak HTTPS.
Browser-Native, SDK-Friendly
Every modern programming language has a first-class WebSocket client. The browser ships with one. Python has websockets. Node has ws. Rust has tokio-tungstenite. Go has gorilla/websocket. They all work with our endpoint without any custom protocol code.
// Browser — yes, really. No proxy, no custom client.
const ws = new WebSocket("wss://fra.relay.allenhark.com/v1/stream/tx?api-key=YOUR_KEY");
ws.onopen = () => ws.send(JSON.stringify({ id: "1", tx: BASE64_TX }));
ws.onmessage = (e) => console.log(JSON.parse(e.data));
// → { "id": "1", "status": "accepted", "request_id": "...", "signature": "..." }That's it. You're now streaming transactions to a Solana relay from a webpage. No CORS workarounds, no service worker, no hosted backend just to bridge a custom protocol.
For Python:
import asyncio, json, websockets
async def submit(tx_b64):
async with websockets.connect(
"wss://fra.relay.allenhark.com/v1/stream/tx",
additional_headers={"x-api-key": "YOUR_KEY"},
) as ws:
for i, tx in enumerate(tx_b64):
await ws.send(json.dumps({"id": str(i), "tx": tx}))
print(await ws.recv())Bundles: The Other Half of the Update
Bundles — atomic groups of up to 5 transactions that land together or not at all — were not previously supported by AllenHark Relay at all. The new WebSocket bundle stream changes that.
const ws = new WebSocket("wss://fra.relay.allenhark.com/v1/stream/bundle?api-key=YOUR_KEY");
ws.onopen = () => ws.send(JSON.stringify({
id: "b1",
txs: [BASE64_TX_1, BASE64_TX_2, BASE64_TX_3], // up to 5 txs
}));
ws.onmessage = (e) => {
const r = JSON.parse(e.data);
// { "id": "b1", "status": "accepted", "bundle_id": "...", "signatures": ["..."] }
};Bundle convention: one relay tip in any transaction of the bundle covers the entire group (wallets whitelisted on the optional Relay Dedicated upgrade need none). Bundles are forwarded only through bundle-aware paths so atomicity is preserved end-to-end — they either land together or not at all.
That makes the bundle stream the right tool for things like:
- Atomic backruns that must execute immediately after a target swap
- Multi-leg arbitrage where partial execution would leave you exposed
- Token launches that need a creator instruction and a snipe to land in lockstep
- NFT mints with a payment + claim that must succeed or fail together
Many In-Flight Requests, One Connection
Each WebSocket message you send is processed in its own task on the relay. You don't have to wait for the response to one transaction before sending the next — fire as many as your rate limit allows, and the responses come back tagged with the id field you supplied so you can correlate them on your end.
for (const tx of allMyTxs) {
ws.send(JSON.stringify({ id: tx.localId, tx: tx.b64 }));
}
// Responses arrive out of order, identified by id.This matters for high-frequency strategies. A naive request/response loop limits you to 1 / round-trip-time transactions per second. Pipelined sends over a WebSocket can push that into the thousands.
Rate-limit tokens are still consumed per message — but a rate-limit rejection just rejects that one frame. Your connection stays open and the next message goes through normally.
What You Should Expect for Latency
What you wait for from ws.send() to the response frame is the network leg from wherever you're actually running, plus the relay's own work — no new connection, no new handshake. The handshake tax is paid once at connect and never again.
QUIC on {ip}:4433 still has an edge at the margin (0-RTT reconnects, stream-level multiplexing). But for almost every real-world client — anything that has to run in a browser, anything written in a language without a polished QUIC client — WebSockets are now the default.
Streams are kept alive aggressively, so a connection you opened at boot will stay hot through quiet periods. There's no idle-timeout you need to design around.
Pick the Region Closest to You
| Region | Hostname |
|---|---|
| Frankfurt | fra.relay.allenhark.com (or relay.allenhark.com) |
| Amsterdam | ams.relay.allenhark.com |
| New York | ny.relay.allenhark.com |
| Tokyo | tyo.relay.allenhark.com |
All four regions speak both wss:// and the bundle stream. Pick the one closest to your client and you'll get the lowest one-way latency.
What We Deliberately Didn't Do
- No new auth model. WebSocket auth is the same
x-api-keyheader (or?api-key=query) you already use for HTTPS. Existing keys work without changes. - No new tip rules. The same tip minimum to a registered tip wallet as HTTP and QUIC, (none from wallets whitelisted on the optional Relay Dedicated upgrade). Bundles inherit the same threshold — any single tx in the bundle paying it is enough.
- No incompatible behavior. A transaction submitted over WebSocket goes through exactly the same validation, tip checks, and broadcast pipeline as one submitted over HTTPS or QUIC. Only the transport changes.
Try It
The endpoints are live across all four regions right now. Open a connection, send a transaction, watch it land:
wscat -c "wss://relay.allenhark.com/v1/stream/tx?api-key=YOUR_KEY"
> {"id":"1","tx":"<your base64 transaction>"}
< {"id":"1","status":"accepted","request_id":"...","signature":"..."}Sign up for an API key — one is required on every request, presented at upgrade for WebSocket — and start streaming. If you're moving from the HTTPS endpoint, replace one URL and one verb. That's the whole migration.
If you need bundles, atomic execution, or just don't want to pay the TLS handshake tax on every transaction anymore, the WebSocket stream is the answer.
