Indexing Solana Program Data: Yellowstone vs Substreams vs DIY
A production-focused comparison of Yellowstone geyser plugins, Messari Substreams, and a custom PostgreSQL pipeline for building queryable indexes of on-chain program state — covering throughput ceilings, reorg handling, operational overhead, and when each approach actually earns its complexity.
If you are running trading bots on Solana — whether arbitrage, market-making, or any signal-driven strategy — you eventually need a queryable layer over raw on-chain state. RPC calls are fine for one-shot lookups; they fall apart the moment you want historical fills, account snapshots across slots, or anything resembling a backtestable dataset. Three approaches dominate in practice: Yellowstone geyser plugins, Messari Substreams, and a hand-rolled PostgreSQL pipeline. Each has a real ceiling and a real floor. Here is what those look like in production.
How Solana Data Gets Out of a Validator
Solana validators expose account and transaction updates through the geyser plugin interface — a Rust trait that fires callbacks on account writes, slot status changes, and confirmed transactions. Everything downstream is built on top of this, including the two managed options. The raw firehose is dense: a busy mainnet validator produces somewhere between 50 MB/s and 200 MB/s of serialized account data during peak load, depending on program activity. None of the three approaches escape that physical reality; they just make different trade-offs about where the fan-out and filtering happen.
Yellowstone: Low Latency, High Ops Burden
Yellowstone is a geyser plugin maintained by Triton that exposes a gRPC streaming interface over the raw validator feed. You subscribe with a filter — specific accounts, program owners, transaction signatures — and receive updates within one or two slots of confirmation. Latency to your consumer is typically under 400 ms on a co-located subscriber.
The trade-offs are steep. You need a dedicated validator or a paid Triton endpoint. Running your own validator adds roughly $1,500–$3,000/month in bare-metal plus the engineering cost of keeping it peered and up to date with mainnet forks. Even on managed endpoints, you own the consumer: deserialization, reorg handling, and persistence are entirely your problem. Yellowstone tells you a slot was confirmed; it does not tell you that slot was later rolled back. If your consumer does not track SlotStatus::Rooted separately and reconcile against it, you will occasionally persist data from orphaned slots. We have seen this cause phantom fills in P&L attribution more than once.
Best for: latency-sensitive consumers (sub-second reaction loops, liquidation watchers) where you are willing to own the full stack.
Substreams: Composable but Constrained
Messari Substreams is a different abstraction. You write transformation modules in Rust that compile to WASM, and the Substreams engine — running on a remote cluster — replays historical data through your module and streams the result. The appeal is composability: you can chain modules, reuse community packages, and get deterministic historical replay without managing any infrastructure.
In practice, the ceiling shows up fast. Substreams currently indexes Solana via a Firehose node that lags mainnet by roughly 2–5 minutes on the free tier and around 30–60 seconds on paid endpoints. That is fine for analytics and backtesting; it is fatal for anything latency-sensitive. The WASM sandbox also imposes real constraints: no external I/O, no database calls inside a module, limited standard library. Complex deserialization of nested Anchor account structs requires writing a custom IDL-aware parser in Rust that compiles within the sandbox size limit — doable, but not a weekend project.
Reorg handling is where Substreams earns its keep. The engine guarantees exactly-once delivery across the canonical chain by handling undo blocks natively. Your module receives an undo signal before any reorg'd data and a replacement payload for the correct fork. You do not write that logic; you consume it.
Best for: historical analytics pipelines, backtesting infrastructure, and teams without dedicated infrastructure engineers.
DIY PostgreSQL Pipeline: Maximum Control, Maximum Surface Area
The third path is a custom pipeline: run a validator (or consume from a Yellowstone endpoint), deserialize with Anchor's account decoder, and write to a normalized PostgreSQL schema. We typically land on a schema with an account_snapshots table partitioned by slot, a transactions table with JSONB for instruction data, and materialized views for the analytics queries that trading teams actually run.
Throughput is not the bottleneck — a single Postgres instance with write-ahead log tuned for bulk inserts handles 20,000–30,000 rows per second without exotic hardware. The bottleneck is almost always deserialization: parsing Anchor accounts from raw bytes in a hot loop requires a tight Rust worker, and any schema migration on the IDL means a coordinated redeploy of the deserializer and a backfill of the affected partition range.
Reorg handling is your responsibility end to end. The cleanest pattern we have found is a two-phase write: insert with a slot_status column defaulting to processed, then a background worker that promotes rows to rooted when the slot finalizes. Reads against analytics queries filter on slot_status = 'rooted'. The lag this introduces — typically 32 slots, around 13 seconds — is acceptable for everything except real-time risk.
Best for: teams with specific schema requirements, existing Postgres tooling, or queries that do not fit neatly into the Substreams module model.
Choosing Based on What You Are Actually Building
| Use case | Recommended approach |
|---|---|
| Real-time signal with sub-second latency | Yellowstone + custom consumer |
| Backtesting and historical analytics | Substreams |
| Custom schema, complex joins, existing BI tooling | DIY PostgreSQL |
| Small team, no infra headcount | Substreams or managed Yellowstone endpoint |
The practical answer for most production deployments is a hybrid: Yellowstone feeding a low-latency hot path and a Substreams pipeline filling in historical depth. Running both in parallel with a slot-range reconciliation job gives you a complete dataset without betting the strategy on a single ingestion path. The operational cost of that redundancy pays for itself the first time one source goes dark during a volatile session.
One thing worth emphasizing: none of these approaches substitute for understanding the program's account model. If you do not know whether a given program writes to a single global state account or thousands of per-user PDAs, the ingestion architecture does not matter — you will be filtering the wrong thing at the wrong layer regardless of which stack you chose.
If you are building out a Solana data infrastructure layer and want to talk through the specifics of your program model and query patterns, reach out directly — this is exactly the kind of problem we work through with teams before committing to an architecture.
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article