NoLimitNodes
PricingDocsBlogAbout
SupportContact
Log in
Blog/Guides

Yellowstone gRPC vs WebSockets: choosing a real-time Solana data pipeline

Both transports stream the same chain, but they come from different places inside the validator and fail in different ways. A field guide to choosing and operating the right pipeline for your workload.

N
NoLimitNodes Data · NoLimitNodes Engineering
Streaming & Decoding Team
May 19, 2026updated Jun 9, 202616 min read
On this page +
  • 01Two doors into the validator
  • 02WebSocket subscriptions in practice
  • 03Yellowstone gRPC in practice
  • 04Latency and payload: the numbers
  • 05Failure modes and recovery
  • 06Choosing your pipeline
  • 07When you want neither

Every real-time Solana system eventually faces the same fork in the road: WebSocket subscriptions or Yellowstone gRPC. They look like two flavors of the same thing, since both push you chain events as they happen. But they originate from different subsystems inside the validator, carry different data, and fail in different ways. Picking wrong costs you either months of unnecessary plumbing or hundreds of milliseconds you can't get back.

2
transports, one chain
RPC PubSub vs Geyser-fed gRPC
~60–70%
smaller payloads
Protobuf vs JSON for the same transaction
300–800ms
typical edge
gRPC processed vs WS confirmed, first sight
1
replay mechanism
Only gRPC can resume from a slot

01Two doors into the validator#

A common misconception is that WebSocket subscriptions are “Geyser with JSON.” They're not. The WebSocket PubSub API is part of the validator's built-in RPC service, fed by the same internal notifications that serve HTTP reads. Yellowstone gRPC is a Geyser plugin: a shared library loaded into the validator process that taps account writes, transactions, entries, and block metadata at the moment the runtime commits them, then serves them over its own gRPC server.

validator internals · where each transport taps the runtime
                    ┌─────────────────────────────────────────┐
                    │              solana validator           │
                    │                                         │
   replay/banking ──┼──▶ runtime commits state                │
                    │         │                               │
                    │         ├──▶ geyser plugin interface    │
                    │         │       │                       │
                    │         │       └──▶ yellowstone gRPC ──┼──▶ protobuf stream
                    │         │            (in-process)       │    (accounts, txs,
                    │         │                               │     slots, blocks)
                    │         └──▶ rpc service                │
                    │                 │                       │
                    │                 └──▶ pubsub websocket ──┼──▶ JSON notifications
                    │                      (accountSubscribe, │    per subscription
                    │                       logsSubscribe, …) │
                    └─────────────────────────────────────────┘
Fig. 1: Both transports see the same chain, but Geyser taps the runtime directly while PubSub rides the RPC service's notification machinery.

That architectural difference drives everything downstream: what data you get, how fresh it is, how it's encoded, and what happens when something breaks.

02WebSocket subscriptions in practice#

The PubSub API gives you a JSON-RPC subscription per question: accountSubscribe for one account's changes, programSubscribe for every account owned by a program, logsSubscribe for transaction logs mentioning an address, slotSubscribe and blockSubscribe for chain progress. It speaks the same commitment levels as HTTP reads, works from any language with a WebSocket client, and requires zero schema tooling. Watching PumpFun activity is a dozen lines:

ws-pumpfun.ts
typescript
import WebSocket from 'ws'

const PUMP_FUN = '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P'
const ws = new WebSocket(process.env.WSS_URL!)

ws.on('open', () => {
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'logsSubscribe',
    params: [
      { mentions: [PUMP_FUN] },          // txs that mention the program
      { commitment: 'confirmed' },
    ],
  }))
})

ws.on('message', raw => {
  const msg = JSON.parse(raw.toString())
  if (msg.method !== 'logsNotification') return
  const { signature, err, logs } = msg.params.result.value
  if (err) return                         // failed tx — usually skip
  // You get log lines, not decoded state. Parsing is on you.
  if (logs.some((l: string) => l.includes('Instruction: Buy'))) {
    console.log('buy:', signature)
  }
})

// Providers drop idle/zombie sockets. Heartbeat or be culled.
setInterval(() => ws.ping(), 30_000)

Honest assessment of what you just built:

  • It was fast to write. No protobufs, no codegen, no client library. This is the transport's genuine superpower.
  • You received log lines, not state. logsSubscribe hands you strings; recovering amounts, mints, and accounts means parsing instructions yourself or making follow-up HTTP calls, which adds back the latency you subscribed to avoid.
  • JSON costs you twice. Once on the wire (base64-in-JSON inflation), once in JSON.parse at 5,000 notifications per second.
WARNING / the silent gap
WebSocket subscriptions have no replay. If your socket drops for four seconds (a deploy, a GC pause, a transient network blip), the chain kept moving and those notifications are simply gone. The protocol won't tell you what you missed, and logsSubscribe won't backfill. Production WS consumers need a reconciliation path (HTTP catch-up reads) bolted on the side.

03Yellowstone gRPC in practice#

Yellowstone inverts the model. You open one bidirectional stream and send one SubscribeRequest describing everything you want: transactions touching certain accounts, account writes for certain owners, slots, blocks, entries. All of it is filtered server-side inside the plugin before a single byte crosses the network. The same PumpFun watcher, upgraded:

grpc-pumpfun.ts
typescript
import Client, { CommitmentLevel } from '@triton-one/yellowstone-grpc'

const PUMP_FUN = '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P'
const client = new Client(process.env.GRPC_URL!, process.env.GRPC_TOKEN, {})

const stream = await client.subscribe()

// One stream, server-side filtering. The validator's Geyser plugin
// pushes us full transactions — accounts, instructions, balances —
// not just log lines.
stream.write({
  commitment: CommitmentLevel.PROCESSED,
  transactions: {
    pump: {
      accountInclude: [PUMP_FUN],
      accountExclude: [],
      accountRequired: [],
      vote: false,
      failed: false,
    },
  },
  accounts: {}, slots: {}, blocks: {}, blocksMeta: {},
  entry: {}, transactionsStatus: {}, accountsDataSlice: [],
})

stream.on('data', update => {
  if (update.transaction) {
    const slot = update.transaction.slot
    const tx = update.transaction.transaction
    // Binary, complete, and ~hundreds of ms ahead of confirmed WS.
    handleTransaction(slot, tx)
  }
  if (update.ping) stream.write({ ping: { id: update.ping.id } })
})

What changed, concretely:

  • Full transactions, not logs. Instructions, account keys, pre/post balances, metadata. Decoded amounts are a borsh-parse away, with no follow-up reads.
  • processed at the source. Geyser emits the moment the runtime commits, so you see activity slots before a confirmed WS notification describes it.
  • Protobuf on the wire. Smaller payloads, cheaper decode, and a typed schema that fails at compile time instead of at 3 a.m.
  • The cost is real tooling. A gRPC client, TLS config, token auth, ping handling, and a build step. Budget a day, not an hour.

04Latency and payload: the numbers#

We measured both transports side by side on the same machine, subscribed to the same program, against endpoints in the same datacenter, recording when each transport first told us about each transaction:

Transport · commitmentp50 first sightp99 first sightAvg payload / tx
Yellowstone gRPC · processed+18ms+61ms1.4KB
Yellowstone gRPC · confirmed+440ms+870ms1.4KB
WebSocket logsSubscribe · processed+74ms+215ms3.9KB
WebSocket logsSubscribe · confirmed+512ms+1,090ms3.9KB
Table 1: First-notification timing for identical PumpFun transactions, 1-hour window, ~41k transactions, same-region consumer. Baseline (0ms) = Geyser emit time at processed commitment.

Two things worth internalizing. First, commitment dominates transport: gRPC at confirmed is slower than WebSocket at processed, because waiting for the cluster to vote costs more than any serialization format saves. Second, at equal commitment, gRPC still wins by 50 to 150ms at the tail. The JSON encode/decode and the PubSub notification machinery are pure overhead the Geyser path never pays.

Choose your commitment level first; that's a correctness decision. Then choose the transport that delivers that commitment with the least machinery between the runtime and your code.
how we frame it for customers

05Failure modes and recovery#

Steady-state latency is what gets benchmarked; recovery behavior is what gets you paged. The transports diverge sharply here.

WebSocket: detect and reconcile

A dropped WS connection loses everything in the gap. Your recovery story is to detect fast (heartbeats, idle timers), resubscribe, then reconcile what you missed via HTTP reads. Accept that for log-shaped data, perfect reconciliation may be impossible.

gRPC: resume from slot

Yellowstone keeps a short in-memory slot buffer and accepts a fromSlot on subscribe. Track the last slot you processed, and a reconnect becomes a resume instead of a gap:

grpc-resume.ts
typescript
let lastSlot = 0n

async function consume(): Promise<never> {
  for (;;) {
    try {
      const stream = await client.subscribe()
      stream.write(buildRequest({
        // Resume from where we left off. Yellowstone replays the gap
        // from its slot buffer instead of silently skipping it.
        fromSlot: lastSlot > 0n ? String(lastSlot) : undefined,
      }))
      for await (const update of stream) {
        if (update.transaction) {
          lastSlot = BigInt(update.transaction.slot)
          await handleTransaction(update.transaction)
        }
        if (update.ping) stream.write({ ping: { id: update.ping.id } })
      }
    } catch (err) {
      console.error('stream dropped, resuming from slot', lastSlot, err)
      await new Promise(r => setTimeout(r, 1_000))   // backoff, then resume
    }
  }
}
NOTE / backpressure is your job on both transports
Both transports disconnect consumers that can't keep up; buffering unbounded updates for a slow client would melt the server. If your handler does real work (DB writes, downstream calls), decouple it from the read loop with a queue, and treat queue depth as a first-class production metric.

06Choosing your pipeline#

Our decision table, distilled from operating both fleets:

WorkloadPickWhy
Trading bot, MEV, market makinggRPC · processedEvery millisecond is alpha; needs full tx detail and resume-from-slot
Wallet UI, balance & activity updatesWebSocket · confirmedHuman-speed updates; zero tooling; accountSubscribe is exactly the shape you need
Indexer / analytics backfill + live tailgRPC · confirmedCompleteness matters more than speed; fromSlot makes the pipeline restartable
Alerting & monitoringWebSocket · confirmedMinutes of build time; occasional gaps tolerable with periodic HTTP reconciliation
Prototyping, hackathonsWebSocketShip today; the migration path to gRPC is well-trodden when you outgrow it
Table 2: Transport selection by workload. 'Both' means start with WS and graduate when the latency or completeness ceiling hurts.

07When you want neither#

There's a third option this comparison quietly assumes away: not running the pipeline at all. Both transports hand you raw chain data. You still own program-specific decoding, IDL drift when protocols upgrade, and the unglamorous work of keeping parsers correct across 37 programs' worth of instruction layouts.

That layer is a product. Our Enhanced Streams deliver decoded, schema-stable events (swaps, launches, graduations, liquidity changes) over the same WebSocket and gRPC transports, and Program Streams expose every instruction and event of every major program as 1,074 individually subscribable topics. The transport trade-offs in this post still apply; the decoding burden doesn't.

///

The summary we'd put on a sticky note: WebSocket is the right default; Yellowstone gRPC is the right ceiling. Start with the transport you can ship this afternoon, instrument your first-sight latency honestly, and when the numbers in Table 1 start costing you money, the gRPC migration is a well-marked road. Both doors are on our endpoints. Run the comparison yourself.

///Related resources
Yellowstone gRPC nodes

Managed gRPC endpoints for low-latency Solana streams.

Enhanced Streams

Decoded Solana events for bots, dashboards, and analytics.

Solana RPC nodes

Private Solana RPC endpoints for builders and trading systems.

Pricing

Flat-rate plans for RPC, WebSocket, gRPC, and streams.

#yellowstone#grpc#websocket#geyser#streaming
N
NoLimitNodes Data
Streaming & Decoding Team

We build the parsing pipeline behind Enhanced Streams and Program Streams: 37 programs, 1,074 topics, decoded in real time.

N
NoLimitNodes Engineering
Infrastructure Team

The team that runs our RPC, WebSocket, gRPC, and streaming fleet. We write about what we operate: validators, Geyser pipelines, and the request paths in between.

On this page
  • 01Two doors into the validator
  • 02WebSocket subscriptions in practice
  • 03Yellowstone gRPC in practice
  • 04Latency and payload: the numbers
  • 05Failure modes and recovery
  • 06Choosing your pipeline
  • 07When you want neither
↑ back to top
///Read next
GuidesJun 19, 2026

Yellowstone gRPC in Python (2026): Setup, 5 Core Patterns & a Real-Time PumpFun Detector

A complete Python guide to Yellowstone gRPC: proto generation, a reusable auth helper, five working patterns from wallet watcher to memcmp filter, a full PumpFun token-launch detector, and production reconnect with exponential backoff.

#yellowstone#grpc#python
18 min read
GuidesMay 15, 2025

Solana Meme Coin Trading with NoLimitNodes API: Ride the Solana Pump Fun with Price Alerts and Token Launch Insights

Solana meme coin trading rewards the people who see the data first. Charts alone won't get you there.

#solana#pumpfun#trading
5 min read
← Older
Pulse of Solana & Pump.fun: What’s Shaping Today’s Market?
Newer →
The anatomy of a sub-50ms Solana RPC request
Run it yourself

Every benchmark in this blog runs against our public endpoints.

Spin up an RPC, WebSocket, or gRPC endpoint in under a minute. Flat pricing, no request caps. Reproduce the numbers for your own workload.

See pricing

Ready to get started?

Get your free API key and start building in under 30 seconds.

Talk to Sales
NoLimitNodes

Solana RPC infrastructure built for performance and scale.

RPC Access
  • HTTP RPC
  • WebSocket
  • gRPC
Infrastructure
  • Compute Platform
  • VPS
  • VDS
  • Bare Metal
  • Geyser Plugin Hosting
Enhanced Streams
  • PumpFun
  • PumpSwap
  • Raydium
  • Orca
  • Meteora
  • System Events
  • Browse All →
Program Streams
  • PumpFun
  • PumpSwap
  • Raydium CLMM
  • Orca Whirlpool
  • Meteora DLMM
  • Jupiter Swap
  • Jupiter Perps
  • Kamino Lending
  • Browse All 37 →
Trading
  • EZWallet
Analytics
  • Historical Datasets
  • Historical Raw Blocks
Company
  • About
Resources
  • Pricing
  • Custom Development
  • Documentation
  • Blog
  • Support
  • Contact Sales
Compare
  • Yellowstone gRPC vs LaserStream
  • Triton vs Helius
  • Raydium API vs Helius
  • PumpSwap API vs Bitquery
  • QuickNode Streams vs NLN
  • All comparisons →
Legal
  • Terms & Conditions
  • Privacy Policy
© 2026 CLR3 Inc., operating as NoLimitNodes. Registered in Ontario, Canada. All rights reserved.solana mainnet