Rust vs TypeScript on Solana: Building a High-Throughput Pump.fun On-Chain Indexer (Modular, Scalable, 429-Safe)
Introduction Solana’s high-throughput architecture makes it a fantastic chain for real-time applications, but it also forces you to design your systems around speed, concurrency, and resilience. Over the past few weeks, I built a Pump.fun on-chain listener in both Rust and TypeScript — not for trading, but to understand how Solana log events work, how to build a clean indexing pipeline, and how to design extensible modules that scale without becoming fragile.
Pump.fun emits a constant stream of program logs, and reading them reliably — especially under network rate limits — requires careful handling. This article walks through my experience building a modular, rate-limit-safe, and high-throughput Solana on-chain indexer in both languages, comparing patterns and tradeoffs along the way.
Why Build an On-Chain Listener?
Understand Solana’s on-chain log system deeply
To know how program logs, signatures, and inner instructions flow through the network.
Compare Rust vs TypeScript for real-time blockchain indexing
Rust is fast and predictable; TypeScript is flexible and fast to build.
Build a modular and extensible indexer
New event types and program versions should “plug in” cleanly.
Handle rate limits (429s) without crashing
Public and private RPC endpoints frequently throttle WebSocket subscriptions.
Architecture Overview: The architecture is intentionally simple and focused:
- A connection manager * Manages the WebSocket subscription * Handles automatic retries and backoff on errors (especially 429s)
- A modular event pipeline * Each event type has its own handler * Handlers can be toggled or extended via config
- A log processor * Receives raw Solana logs * Extracts relevant Pump.fun event data * Passes them downstream
- A configuration layer * Enables/disable modules without code changes * Determines which event handlers run * Allows different pumps, programs, or RPCs per environment
Handling RPC Rate Limits (429s) One of the biggest problems when listening to Pump.fun logs is rate limiting. 1. Retry loop around subscription setup This is where 429s actually happen.
2.Exponential backoff for fairness Avoids hammering RPC providers.
3.Isolated error boundaries processLog failures don’t crash the listener.
4. Optionally rotating RPC endpoints (Helius → Triton → dRPC → private RPC)
This ensures the indexer stays alive for days or weeks without manual intervention.
Rust vs TypeScript — Final Thoughts Rust is ideal when you want maximum throughput
- low-latency log handling
- long-running reliability
- static guarantees and zero-cost abstractions
async fn listen(&self) -> Result<()> {
let pubsub = PubsubClient::new(&self.config.network.rpc_wss_url).await?;
let pumpfun_pubkey = Pubkey::from_str(&self.config.programs.pump_fun)?;
let (mut stream, unsubscribe) = pubsub
.logs_subscribe(
RpcTransactionLogsFilter::Mentions(vec![pumpfun_pubkey.to_string()]),
// RpcTransactionLogsFilter::All,
RpcTransactionLogsConfig {
commitment: Some(CommitmentConfig::confirmed()),
},
)
.await?;
info!("Subscribed to Pump.fun program");
while let Some(result) = stream.next().await {
let rpc_log: RpcLogsResponse = result.value;
if rpc_log
.logs
.iter()
.any(|l| l.contains(&pumpfun_pubkey.to_string()))
{
if let Err(e) = self.process_log(rpc_log.clone()).await {
// error!("Pump.fun process_log error: {:?}", rpc_log);
error!("Error processing Pump.fun log: {}", e);
}
}
}
unsubscribe().await;
Ok(())
}
TypeScript is ideal when you want:
- fast iteration
- flexible workflows
- easy integration with dashboards/APIs
- rapid prototyping or proof-of-concept listeners
private async listen() {
while (true) {
try {
console.log("Attempting subscription...");
const subscriptionId = await this.connection.onLogs(
this.pumpFunProgramId,
async (logs, ctx) => {
try {
await this.processLog(logs);
} catch (err) {
console.error("Error processing log:", err);
}
},
"confirmed"
);
console.log("Subscribed with ID:", subscriptionId);
// Keep the listener alive forever
await new Promise(() => {});
} catch (err: any) {
const msg = err?.message ?? String(err);
console.error("Subscription failed:", msg);
if (msg.includes("429") || msg.includes("rate")) {
console.warn("Rate limited. Retrying in 45 seconds...");
await this.sleep(45000);
continue;
}
console.warn("Unexpected error. Retrying in 10 seconds...");
await this.sleep(10000);
}
}
}
Conclusion Whether you’re using Rust or TypeScript, the core ideas — modularity, retry logic, clean architecture — apply to any blockchain indexing pipeline. If you’re exploring Solana development or architecting real-time systems, building your own listener is one of the best ways to understand the chain’s internals.
Read the full article here: https://medium.com/@hakeemhal/rust-vs-typescript-on-solana-building-a-high-throughput-pump-fun-bc7551f5a649