·5 min read

Streaming Live Solana Trades Through Cloudflare Workers (The Wire-Splice Trick)

build-in-publicrustsolananextjscloudflaressereal-time|
PostLinkedIn

Streaming Live Solana Trades Through Cloudflare Workers (The Wire-Splice Trick)

Cloudflare Workers "can't hold long-lived connections." So how is my Next.js app streaming live Solana trades through one, with the bytes flowing continuously for as long as the tab is open?

The answer is the most fun part of this build, and it's a little counterintuitive: the Worker isn't a stream server. It's a wire splice. But before we get there, let me walk the whole pipeline, because the splice only makes sense once you see what's on both ends of it.

1. Ingest: Solana into a database

The system of record starts with a long-running Rust/Axum service that subscribes to a Solana validator over a Yellowstone/Geyser gRPC stream — account and transaction updates pushed in real time. The service parses those updates and persists them: Postgres for the structured stuff (markets, swaps, positions, time-series), Redis as a cache layer, RocksDB for raw state snapshots, plus a search index. That's the durable truth. Everything downstream is a view onto it.

2. Fan-out: one subscription, many clients

Here's a constraint that shapes everything: you get one Geyser subscription, but many browsers want the data. So inside the process I fan it out with tokio::sync::broadcast channels — separate channels for market updates, user updates, and global events.

Two design choices I'd defend in any review:

  • Fire-and-forget publish. Broadcasting an update never blocks the ingest write path. The thing that writes to Postgres must never wait on a slow websocket consumer.
  • Explicit lagged events. When a slow client falls behind the broadcast ring buffer, it doesn't silently drop data and quietly drift out of sync. It gets a lagged event and resyncs from the DB. The stream self-heals instead of lying to you.

3. Stream: Rust to the browser over SSE

The edges are Axum SSE endpoints — /stream, /markets/{addr}/stream, /users/{owner}/stream. Two things SSE forces you to get right on a real server:

  • These routes deliberately bypass the global request-timeout layer. A request timeout is correct for normal handlers and fatal for a stream that's supposed to stay open forever.
  • 15-second keep-alive pings, so idle proxies and load balancers don't decide the connection is dead and cut it.

4. The centerpiece: a two-connection pass-through

Now the fun part. The browser opens an EventSource to a same-origin Next.js Route Handler — which runs on a Cloudflare Worker at the edge.

There are really two chained TCP connections here: browser ↔ Worker, and Worker ↔ Rust. The naive implementation reads the upstream stream, parses each event, and re-emits it. Don't. The Worker should not consume and rebuild anything. It does this:

return new Response(upstream.body);

That's the whole idea. You take the upstream response body — a ReadableStream — and hand it straight back as your own response body. The two connections get spliced into a single byte pipe. Every data: chunk Rust writes flows straight through to the browser, untouched.

"But Workers run on a V8 isolate, they can't sit on a connection for an hour." Right — they can't sit busy for an hour. But piping is I/O-bound. CPU-time doesn't tick while bytes are merely flowing; wall-clock is uncapped as long as the client stays connected. The Worker is near-idle plumbing, not a busy process. That's the loophole, and it's a legitimate one.

The header gotcha that makes or breaks it

This is where people lose an afternoon. Any layer that thinks it's holding a complete body will try to buffer or compress it — and buffering an infinite stream means it hangs forever. The browser shows a skeleton loader that never resolves. The connection is "open." Nothing arrives.

The fix is to forbid every form of buffering on the way out:

  • cache-control: no-store
  • x-accel-buffering: no
  • strip content-encoding — no gzip on a stream that never ends
  • strip content-length — there is no length; it's infinite
Get those right and the skeleton resolves instantly, then updates live.

5. Fold deltas into React Query

On the client, each SSE delta is merged into the React Query cache — merge in place for the high-frequency events, invalidate-and-refetch for the rarer ones. No polling. The UI just updates as trades land.

What I took away

The lesson that stuck: the cleanest streaming layer is the one that does the least. The Worker's job wasn't to be smart — it was to get out of the way and let two TCP connections become one. The hard part wasn't the pipe; it was convincing every layer in between to stop trying to help.

Oleksandr Yusypenko

Oleksandr Yusypenko

Senior Full-Stack + AI Engineer. Building in public — AI agents, LangGraph, production systems.