Polling vs WebSockets vs gRPC on Solana: The Real Differences

Every bot, every dashboard, every trading tool on Solana starts with the same question: how do I get data off the chain? The answer you choose determines everything that follows. Your latency ceiling. Your reliability during congestion. Whether you see an opportunity or miss it entirely because your data was 2 seconds stale.

There are three ways to get real-time data from Solana: polling the RPC, subscribing via WebSockets, or streaming through Yellowstone gRPC. Each one was born from a different era of the network's evolution, each one solves a different set of problems, and each one breaks in different ways when the network gets busy.

This is not a surface-level comparison. We're going deep into the architecture, the failure modes, and the real-world performance characteristics that determine which approach belongs in your stack.

Polling: The Default Everyone Starts With (But You're Reading This, So You Aren't Just Anyone)

Polling is the simplest model. Your application makes an HTTP request to a Solana RPC node, gets a JSON response back, and repeats. Every few hundred milliseconds, you call getAccountInfo to check if an account changed. You call getProgramAccounts to scan for all accounts owned by a specific program. You call getTransaction to look up what happened in a specific transaction.

It works exactly like any other REST API. You ask a question, you get an answer.

Your App → HTTP Request → RPC Node → JSON Response → Your App
(repeat every N milliseconds)

The Solana JSON-RPC exposes dozens of methods. getAccountInfo returns the data and balance for a single account. getProgramAccounts returns every account owned by a given program, with optional memcmp and dataSize filters. getSlot tells you the current slot number. getSignaturesForAddress gives you transaction history. getLatestBlockhash gets you a fresh blockhash for building transactions.

For wallets, dashboards, and anything where "close enough" is good enough, polling works fine. It's simple to implement, easy to debug, and every RPC provider supports it out of the box.

Where Polling Falls Apart

The problem is fundamental: you're always behind. Every poll is a snapshot of state that may already be outdated by the time you receive it. On a chain that produces blocks every 400 milliseconds and processes thousands of transactions per second, the gap between "what the chain looks like now" and "what your last poll told you" is a gap where money moves.

If you're polling every 500ms, you're seeing the market on a 500ms delay at best. In practice it's worse. The RPC node itself trails the current leader by 2 to 5 slots (roughly 800 to 2000ms) due to confirmation processing and network propagation. Add your poll interval on top of that, and you're looking at 1 to 3 seconds of effective lag.

Then there's the waste. Most of your polls return the same data as the last one. The account didn't change. The slot is the same. You burned an API call, the server burned CPU serializing a JSON response, and neither of you learned anything. Multiply that across every account you're tracking and every bot running on the same infrastructure, and you start to understand why RPC providers enforce strict rate limits.

getProgramAccounts deserves special mention. It's the most expensive RPC call on Solana. Without filters, it scans every account owned by a program, which for something like the Token Program means millions of accounts. Even with memcmp filters, it requires significant server-side compute. Bare-metal providers return results in under 500ms. Shared RPC endpoints can take 5 to 10 seconds. Some providers charge 10x the credits for this single method compared to lighter calls.

And the most dangerous failure mode of polling is silent staleness. When the network is congested, RPC nodes don't return errors. They return data. The data just happens to be stale. Your bot thinks it's seeing the current price, but the price moved three slots ago. There's no flag, no warning, no way to know unless you're independently verifying slot numbers. This is how bots make trades on outdated state and wonder why they got filled at the wrong price.

WebSockets: The Push Model That Almost Works

WebSockets were Solana's answer to the polling problem. Instead of your application asking for data over and over, the chain pushes updates to you. Open a persistent connection, subscribe to the accounts or programs you care about, and receive notifications the moment something changes.

Your App ←→ Persistent WebSocket Connection ←→ RPC Node
(server pushes updates when state changes)

Solana's WebSocket API supports several subscription types. accountSubscribe notifies you when a specific account's data changes. programSubscribe notifies you when any account owned by a program changes, with optional memcmp and dataSize filters. logsSubscribe pushes transaction logs, optionally filtered by a mentioned address. signatureSubscribe tells you when a specific transaction reaches a given commitment level. slotSubscribe fires on every new slot.

The improvement over polling is immediate. No more wasted calls returning unchanged data. No more artificial lag from poll intervals. When an account updates, your application knows within the same slot. For basic monitoring, wallet notifications, and UI updates, WebSockets are a significant step up.

The Reliability Problem

But Solana WebSockets have a well-documented history of reliability issues that makes them dangerous for anything where missed data has consequences.

The most serious problem is silent notification drops. GitHub issue #18587 documented programSubscribe silently missing confirmed transactions on mainnet. A developer testing against the stake program received notifications for accounts 1, 2, 4, and 6 while accounts 3 and 5 never arrived. A validator operator independently confirmed that logsSubscribe received only 4 events from 9 unique transactions. The root cause sits at the validator runtime level: there are confirmed slots with program account changes that simply do not emit notifications.

This is not a theoretical edge case. It is the documented behavior of the system.

Issue #35489 reported 15-second average delays for finalized commitment level subscriptions. When a node falls slightly behind and then catches up by processing 2 to 3 slots rapidly, the WebSocket stream cannot handle the burst. Data is silently dropped. Issue #35506 documented slotSubscribe stopping entirely with no error. Issue #19072 showed that the u/solana/web3.js library swallows subscription errors silently, so your callback simply stops firing and you have no way of knowing.

The PubSub system becomes unresponsive at around 1000 concurrent connections. Connections drop silently after 10 minutes of inactivity if you don't send pings. And when the send buffer on the server fills up during high traffic, data is discarded with no notification to the client.

The Architectural Bottleneck

There's a deeper issue that explains many of these problems. Solana's standard WebSocket implementation waits until the end of each slot before sending update notifications. It batches changes rather than streaming them as they occur within the slot. This means even in the best case, there's up to 400ms of artificial latency baked into the architecture, because the system holds updates until the slot boundary before flushing them to subscribers.

This batching also creates the burst problem. When multiple slots worth of data flush at once (during catch-up or micro-forks), the WebSocket layer has to serialize and transmit all of it simultaneously. If the client can't consume fast enough, the server's TCP send buffer fills. There is no backpressure signaling. There is no "slow down" message from client to server. The buffer fills, data is dropped, and the client never knows.

All of this adds up to a system where WebSockets work most of the time, but fail in exactly the moments that matter most. During token launches, liquidation cascades, and high-activity periods, which are the precise moments when real-time data is most critical, WebSockets are the first thing to break.

Production applications built on Solana WebSockets end up carrying significant defensive code: reconnection logic with exponential backoff, heartbeat pings every 60 seconds, state reconciliation after reconnects, deduplication for events that arrive twice, and gap detection for events that never arrive at all. The infrastructure you build to compensate for WebSocket unreliability often exceeds the complexity of the application logic itself.

Yellowstone gRPC: Streaming From the Source

Yellowstone gRPC represents a fundamentally different approach. Instead of asking an RPC node for data (polling) or waiting for an RPC node to push data to you (WebSockets), gRPC streams data directly from the validator's internal memory the moment it changes.

Validator Memory (AccountsDb / Bank)
        ↓ (Geyser plugin callback)
    Serialize to Protocol Buffers
        ↓ (gRPC stream over HTTP/2)
    Your Application

The key difference is where the data comes from. Both polling and WebSockets go through the JSON-RPC layer, which means the data has to be written to the validator's state, then read back, then serialized to JSON, then transmitted over HTTP or WebSocket. Geyser plugins bypass all of that. They hook directly into the validator process itself and emit data the moment an account is written or a transaction is processed, before it ever touches the RPC layer.

How Geyser Works Under the Hood

The Geyser plugin system was introduced in late 2021 after Solana Labs recognized that RPC polling couldn't keep up with the demands of high-frequency trading, MEV searching, and large-scale indexing. The original approach was AccountsDb replicas, separate processes that mirrored the validator's state. This proved too complex to keep synchronized, so they built a plugin interface instead.

A Geyser plugin is a dynamic shared library (a .so file on Linux) that loads directly into the validator process. This is not a sidecar. It runs in the same memory space as the validator itself. The plugin implements a trait with callback methods that the validator invokes at key moments:

update_account() fires on every account write during transaction processing. notify_transaction() fires for each transaction as it's processed. update_slot_status() fires when a slot transitions between processed, confirmed, and rooted states. notify_block_metadata() fires when a block is completed with its hash, rewards, and timestamp.

Because the plugin runs inside the validator process, it sees data at the earliest possible moment. Account updates arrive before they're written to disk. Transactions are visible before they reach confirmed commitment. This is the processed commitment level, the fastest data Solana can provide.

Triton One built the first and most widely adopted gRPC implementation on top of this plugin interface. They called it Yellowstone (and the gRPC component Dragon's Mouth). It takes the raw callbacks from the Geyser interface, serializes the data into Protocol Buffer messages, and streams them over gRPC to connected clients.

The Subscription Model

Where gRPC truly separates itself from WebSockets is in what you can filter.

A WebSocket programSubscribe lets you filter by owner program, with optional memcmp and dataSize. That's it. You get every account update for the entire program, serialized as JSON, whether you want it or not. If you're subscribing to the Token Program, you're drinking from a firehose.

A gRPC subscription request lets you combine multiple filter types in a single connection. Transaction filters support include and exclude arrays of account addresses, vote transaction exclusion, failed transaction exclusion, and individual signature subscriptions. Account filters support multiple pubkeys, multiple owner programs, memcmp, dataSize, and token account state filters. You can even request partial account data through accountsDataSlice, specifying an offset and length to receive only the bytes you care about rather than the full account.

Here's what a PumpFun transaction subscription looks like:

let mut transactions = HashMap::new();
transactions.insert(
    "pumpfun_events".to_owned(),
    SubscribeRequestFilterTransactions {
        vote: Some(false),
        failed: Some(false),
        account_include: vec![PUMPFUN.to_string()],
        account_exclude: vec![],
        account_required: vec![],
        signature: None,
    },
);

let request = SubscribeRequest {
    accounts: HashMap::default(),
    slots: HashMap::default(),
    transactions,
    transactions_status: HashMap::default(),
    blocks: HashMap::default(),
    blocks_meta: HashMap::default(),
    entry: HashMap::default(),
    commitment: Some(CommitmentLevel::Processed as i32),
    accounts_data_slice: vec![],
    ping: None,
    from_slot: None,
};

This gives you every successful, non-vote transaction that touches the PumpFun program. Nothing else. The filtering happens server-side before serialization and transmission, which means your client only receives and processes the data it actually needs.

Compare that to a WebSocket logsSubscribe with a mentions filter for the same program. You'd get JSON-encoded logs for matching transactions, but you wouldn't be able to exclude vote transactions, you wouldn't be able to exclude failed transactions, and if the filter has one of its documented bugs, you'd either get no data or the wrong data.

Protocol-Level Advantages

gRPC runs on HTTP/2, which provides multiplexing (multiple logical streams over a single TCP connection), flow control (the client can signal the server to slow down), and header compression. WebSockets run on raw TCP with no multiplexing and no standardized flow control.

The data is serialized as Protocol Buffers, a binary format that is roughly 5x faster to encode and decode than JSON, with significantly smaller payloads. On a high-throughput subscription where you're receiving thousands of events per second, the difference between parsing JSON strings and deserializing compact binary messages is measurable in CPU utilization.

gRPC also has native support for bidirectional streaming. The client can send updated subscription requests on the same connection without dropping and reconnecting. To modify your filters, you send a new SubscribeRequest, which entirely replaces the previous subscription. To pause without disconnecting, you send an empty request.

The Numbers

Talk is cheap. Here are real-world latency characteristics from production infrastructure:

Slot notification latency (p90)

Polling trails the leader by 2 to 5 slots, which translates to roughly 800 to 2000ms before you even make the request. Add your poll interval and network round-trip, and effective slot awareness is typically 1 to 3 seconds behind the leader.

WebSockets deliver slot notifications at approximately 10ms p90 in ideal conditions. But "ideal conditions" is doing a lot of work in that sentence. Under load, the batching behavior and buffer issues push real-world latency to 1 to 2 seconds, with documented spikes to 15 seconds or more for finalized commitment.

Yellowstone gRPC delivers slot notifications at approximately 5ms p90. Because the data comes directly from validator memory without passing through the RPC layer, the latency floor is fundamentally lower. Under sustained load, dedicated gRPC infrastructure maintains sub-100ms average latency.

Account update latency (p90)

This is where the practical difference matters most for trading. WebSockets deliver account updates at approximately 374ms p90. gRPC delivers them at approximately 215ms p90. That 159ms gap is the gap between your bot and someone else's bot. On a fast-moving bonding curve or a DEX pool rebalance, 159ms is multiple price levels.

Bandwidth efficiency

gRPC's Protocol Buffer encoding produces payloads roughly 3 to 5x smaller than the equivalent JSON. Combined with server-side filtering and data slicing, a gRPC client receiving PumpFun trade events consumes a fraction of the bandwidth that a WebSocket client would use subscribing to the same program via logsSubscribe and filtering client-side.

Congestion: Where the Differences Become Survival

During normal network operation, all three approaches work reasonably well. The real test is what happens when things get busy.

Solana has a history of congestion events. The April 2022 Candy Machine spam attack saw bots firing millions of transactions per second, with nodes reporting 6 million packets per second at over 100 Gbps. Validators ran out of memory and consensus stalled. The February 2023 large block outage pushed the network into vote-only mode.

During events like these, each approach fails differently.

Polling returns stale data that looks correct. The RPC node responds with a 200 status code and a well-formed JSON body. The slot number is present, the account data looks valid, but it's from 5 or 10 slots ago. There's no indication that anything is wrong. This is the most dangerous failure mode because it creates false confidence. Your bot acts on data it believes is current, and there's no error to catch.

WebSockets degrade visibly but unpredictably. Connections drop. Notifications arrive late or not at all. The server's send buffer fills and starts discarding events without telling the client. If your application has robust reconnection logic and gap detection, you'll at least know something went wrong. But you won't know what you missed, and there's no way to replay missed events after reconnecting.

gRPC degrades the most gracefully. HTTP/2's flow control means the server and client can negotiate data rates rather than silently dropping packets. Because the data comes from validator memory rather than the RPC layer, the only bottleneck is the validator itself. If the validator is processing transactions, the Geyser plugin is emitting events. Dedicated non-voting streaming nodes isolate the gRPC workload from consensus, so streaming doesn't compete with block production for resources.

This doesn't mean gRPC is invincible. If the validator itself falls behind due to consensus issues, the Geyser plugin falls behind with it. But the failure is correlated with the validator's actual state rather than an intermediate layer silently losing data.

When to Use What

Polling belongs in applications where latency doesn't matter and simplicity does. Portfolio trackers, analytics dashboards, block explorers, wallet balance displays. Anywhere you're showing data to a human who won't notice a 2-second delay. It's also the right choice for historical queries. getTransaction and getSignaturesForAddress retrieve past data that gRPC and WebSockets can't provide (they only stream live events).

WebSockets occupy an awkward middle ground. They're better than polling for real-time data, but their reliability issues make them risky for anything with financial consequences. The one place they remain essential is the browser. gRPC does not work in web browsers. If you're building a frontend that needs live updates, WebSockets (or polling) are your only options. For backend services, WebSockets have been largely superseded by gRPC in the Solana ecosystem.

gRPC is the right choice for anything where timing, reliability, or throughput matters. Trading bots, copy traders, snipers, liquidation engines, MEV searchers, real-time indexers, DeFi protocol monitors. If missing an event or seeing it 500ms late costs you money, gRPC is not optional. It's the infrastructure standard for serious Solana development.

The ecosystem has voted with its feet. Every major trading desk, MEV operation, and high-performance indexer on Solana runs on Yellowstone gRPC. The question isn't whether to use it. The question is how long you can afford not to.

The Cost Question

gRPC's technical superiority has historically come with a financial barrier. Accessing Yellowstone gRPC through major providers often requires enterprise-tier plans starting at $999/month or more, plus usage-based charges on top.

This pricing model made sense when gRPC was niche infrastructure for well-funded trading operations. But gRPC isn't niche anymore. It's foundational. And pricing it like a luxury locks out independent traders, small teams, and builders in emerging markets who would benefit from it the most.

At Solami, we offer gRPC connections starting at $6/day or $100/month, with no base plan required. You get the same Yellowstone protocol, the same real-time data, the same multi-region infrastructure across Frankfurt, New York, Amsterdam, and Singapore. Our Pro plan at $99/month includes a gRPC connection alongside RPC access and SWQoS transaction routing. Every account gets a free 2-day trial to test the stream before committing.

We built it this way because we believe the gap between "knows about gRPC" and "can afford gRPC" shouldn't be as wide as it is. A developer in Nairobi prototyping a liquidation bot and a fund in Manhattan running production infrastructure both deserve access to the same data stream. The code doesn't care who's running it. The infrastructure shouldn't either.

Getting Started

If you're currently polling and want to move to gRPC, the migration path is straightforward. Replace your polling loop with a gRPC subscription, map your getAccountInfo calls to account subscriptions, map your getProgramAccounts calls to program-filtered transaction subscriptions, and let the stream push data to you instead of pulling it.

If you're on WebSockets and tired of writing reconnection and gap-detection code that's longer than your actual application logic, gRPC eliminates most of that. The stream is more reliable, the protocol handles backpressure properly, and when disconnections do happen, the reconnection model is cleaner.

If you want to see it in action, our open-source PumpFun starter is a fully working copy trader built on Yellowstone gRPC. It connects, subscribes, decodes events, and executes trades. Under 1,400 lines of Rust. Clone it and run it.

Posted Mar 4, 2026, 12:14 PM UTC

10
Loading comments…