Skip to main content
TLDR:
  • Use multiple Solana RPC endpoints to reduce latency and avoid single points of failure.
  • A distributed setup ensures higher availability and faster data updates, especially for real-time balance monitoring.
  • This sample Node.js code with a connection pool approach demonstrates how to subscribe to account balance changes across different geographic locations simultaneously.
  • Environment variables keep your endpoints secure while allowing easy setup and configuration.

Main article

This guide will explore how a distributed approach to querying the Solana blockchain can reduce latency and provide a more resilient infrastructure for your DApps. Whether you’re a blockchain enthusiast or a seasoned developer looking to optimize your Solana-based projects, this tutorial is crafted to offer valuable insights and practical steps to achieve a seamless and efficient blockchain interaction experience.

A brief overview of Solana’s architecture

Solana stands out in the blockchain world for its unprecedented throughput and minimal transaction costs stemming from its innovative architecture. The Proof of History (PoH) consensus mechanism is at the heart of Solana’s efficiency. This unique solution enables greater scalability by creating a historical record proving that an event has occurred at a specific time. This, coupled with the blockchain’s underlying Proof of Stake (PoS) consensus, allows Solana to process thousands of transactions per second without compromising decentralization or security.

A simple project

At the heart of this tutorial lies a practical, hands-on example that will guide us through the intricacies of interacting with Solana’s network using multiple RPC endpoints. We will develop a robust piece of JavaScript code to manage a pool of Solana connections. This code establishes connections to several RPC endpoints across different geographic locations and intelligently distributes operations amongst these endpoints to achieve optimal performance and reliability. Through this example, we aim to listen to the balance changes of a specific Solana account from multiple nodes in parallel. Using multiple nodes in different regions simultaneously can improve reliability and performance, and the first node to get the updated data will win the race.

The importance of multiple endpoints

The traditional approach of interacting with the blockchain through a single RPC endpoint can be a bottleneck for your application. This is where the strategy of using multiple endpoints across various geographic locations comes into play, offering several key advantages:
  • Redundancy: By having your application connected to multiple nodes, you mitigate the risk of a single point of failure. If one node goes down or becomes unreachable, your application can seamlessly switch to another, ensuring uninterrupted service. This is because each node is physically running on a different machine.
  • Reduced Latency: Latency can vary significantly based on the geographic distance between your user and the RPC endpoint. Generally, the endpoint closer to the user will respond faster, but sometimes, another node may pick up new data sooner.
  • Higher Availability: Different nodes may have varying loads or maintenance schedules. Running multiple endpoints ensures that your application always has access to at least one up-to-date and responsive node, thereby improving the reliability of your service.
Throughout this tutorial, we’ll guide you through setting up a Solana connection pool to interact with multiple RPC endpoints efficiently. We aim to empower your applications to leverage Solana’s high-performance blockchain most effectively, ensuring users enjoy a fast, reliable, and seamless experience.

Prerequisites

Before we get into the code, ensuring you have the right tools and setup is crucial. Let’s walk through everything you’ll need:

Environment setup

To interact with the Solana blockchain and execute the scripts you’ll write, certain environmental setups are necessary:
  • Node.js: Install Node.js (version 18 or above) on your machine. Node.js is fundamental for running our scripts and managing dependencies. You can download it from the official Node.js website.
  • npm (Node Package Manager): npm is the default package manager for Node.js and is indispensable for installing the libraries our project requires. It comes bundled with Node.js, so there’s no separate installation process.

Deploy a Chainstack Solana node

Deploy multiple reliable Solana RPC endpoints. Note that you need to be on a paid plan to deploy multiple nodes.
  1. Sign up with Chainstack.
  2. Deploy a node.
  3. View node access and credentials.

Installing required packages

Our project relies on a Solana JavaScript library plus dotenv (which loads environment variables from a .env file into process.env, keeping RPC URLs out of your code). This guide shows both Solana JS libraries side by side — both are actively maintained, so use whichever fits your project:
  • @solana/kit — the newer, tree-shakable, functional SDK. Account subscriptions are exposed through a websocket client (createSolanaRpcSubscriptions).
  • @solana/web3.js — the classic object-oriented API, where a Connection object carries both HTTP and websocket calls (onAccountChange).
To install, navigate to your project directory and run:
Bash
npm install @solana/kit dotenv
Then add this line to your package.json file:
  "type": "module",

Setting up environment variables

We’ll use environment variables to manage our RPC endpoints and other sensitive information. This method keeps our credentials secure and makes our application easily configurable without hardcoding sensitive data:
  1. Create a .env file in the root of your project directory.
  2. Add your Solana RPC URLs and sensitive information to the .env file. For instance:
# HTTPS endpoints
SOLANA_RPC_NODE_1="YOUR_CHAINSTACK_RPC"
SOLANA_RPC_NODE_3="YOUR_CHAINSTACK_RPC"
SOLANA_RPC_NODE_2="YOUR_CHAINSTACK_RPC"

# WSS endpoints
SOLANA_WSS_NODE_1="YOUR_CHAINSTACK_WSS"
SOLANA_WSS_NODE_3="YOUR_CHAINSTACK_WSS"
SOLANA_WSS_NODE_2="YOUR_CHAINSTACK_WSS"

Additional considerations

  • Security: Always keep your private keys and sensitive information secure. Do not commit your .env file or any files containing sensitive data to version control. Make sure to include the .env file in your .gitignore.
With these prerequisites out of the way, you’re now set to dive into the code.

The code

Now that we are set, create a new file in your project named index.js and paste the following code into it.
Javascript
import { createSolanaRpcSubscriptions, address } from "@solana/kit";
import "dotenv/config";

// Account subscriptions are websocket-only, so each pool entry needs a WSS URL.
class SubscriptionPool {
    constructor(endpoints) {
        this.subscribers = endpoints.map(endpoint => ({
            wssUrl: endpoint.wss,
            rpcSubscriptions: createSolanaRpcSubscriptions(endpoint.wss),
        }));
    }
}

let latestLoggedBalance = null; // Shared state to track the latest logged balance

async function listenForBalanceChanges(publicKey, subscriptionPool, abortSignal) {
    const account = address(publicKey);

    // Run one subscription per endpoint concurrently; the first node to see the
    // new data logs it first.
    await Promise.all(subscriptionPool.subscribers.map(async ({ rpcSubscriptions, wssUrl }) => {
        console.log(`Setting up balance change listener for node: ${wssUrl.slice(0,33)}`);

        // subscribe() resolves to an async iterable of notifications
        const notifications = await rpcSubscriptions
            .accountNotifications(account, { commitment: "confirmed" })
            .subscribe({ abortSignal });

        for await (const notification of notifications) {
            const newBalance = notification.value.lamports; // bigint in @solana/kit

            // Check if this balance change has already been logged
            if (latestLoggedBalance !== newBalance) {
                const dateTimeString = new Date().toISOString(); // ISO 8601 format

                console.log(`[${dateTimeString}] Balance change detected on node: ${wssUrl.slice(0,33)}`);
                console.log(`[${dateTimeString}] New balance: ${newBalance} lamports`);
                console.log("======================================================================");

                // Update the shared state with the new balance
                latestLoggedBalance = newBalance;
            }
        }
    }));
}

(async () => {
    const nodeEndpoints = [
        { wss: process.env.SOLANA_WSS_NODE_1 },
        { wss: process.env.SOLANA_WSS_NODE_2 },
        { wss: process.env.SOLANA_WSS_NODE_3 },
    ];

    const subscriptionPool = new SubscriptionPool(nodeEndpoints);
    const publicKey = "G98hD3T33SiJa8WcWgJ9coT5fz1F3ciwJjKnecxxd3Bi"; // Replace with the address you're interested in

    // Listen for balance changes on all nodes; call abortController.abort() to stop
    const abortController = new AbortController();
    await listenForBalanceChanges(publicKey, subscriptionPool, abortController.signal);
})();

Code breakdown

Both versions do the same thing — open one client per endpoint, subscribe to a single account across all of them, and log the first node to report each new balance. They differ only in how the subscription is opened and consumed. Here are the key components:

Imports and environment variables

The @solana/kit version imports createSolanaRpcSubscriptions and address; the @solana/web3.js version imports Connection and PublicKey. Both load dotenv/config to read the endpoints from .env and keep them out of the code.

The connection/subscription pool

Each pool entry holds one client per endpoint:
  • @solana/web3.js builds a Connection per node, passing the HTTP RPC URL plus { wsEndpoint } for its websocket.
  • @solana/kit builds a createSolanaRpcSubscriptions client per node. Account subscriptions are websocket-only, which is why the @solana/kit pool takes just the wss URL for each endpoint.

Shared state for balance tracking

Both keep a single latestLoggedBalance so the same balance isn’t logged twice when multiple nodes report the same change. In @solana/kit this value is a bigint; in @solana/web3.js it’s a number.

Listening for balance changes

This is the core API difference between the two libraries:
  • @solana/web3.js registers a callback: connection.onAccountChange(publicKey, (accountInfo) => { ... }, 'confirmed'), where accountInfo.lamports is a number.
  • @solana/kit opens an async iterable: await rpcSubscriptions.accountNotifications(account, { commitment: 'confirmed' }).subscribe({ abortSignal }), then consumes it with for await (const notification of notifications). Each notification.value.lamports is a bigint. Running one subscription per endpoint under Promise.all keeps them all listening concurrently, and the shared abortSignal tears them all down cleanly.

Main execution flow

Both define the endpoints from environment variables, build the pool, choose a public key to watch, and start listening. The @solana/kit version additionally creates an AbortController and awaits the listeners; call abortController.abort() to stop every subscription at once.

Run the script

To run the script, make sure to place your HTTPS and WSS endpoints in the .env file, then run the start command in the console:
node index
This will start the program and log any new balance change in real-time, like the following:
Setting up balance change listener for node: https://solana-mainnet.core.chain
Setting up balance change listener for node: https://nd-094-012-520.p2pify.com
Setting up balance change listener for node: https://nd-445-788-065.p2pify.com
[2024-04-08T21:00:49.290Z] Balance change detected on node: https://nd-445-788-065.p2pify.com
[2024-04-08T21:00:49.290Z] New balance: 1104861848 lamports
======================================================================
[2024-04-08T21:00:49.927Z] Balance change detected on node: https://nd-445-788-065.p2pify.com
[2024-04-08T21:00:49.927Z] New balance: 1104856848 lamports
======================================================================
[2024-04-08T21:00:51.788Z] Balance change detected on node: https://nd-445-788-065.p2pify.com
[2024-04-08T21:00:51.788Z] New balance: 1104851848 lamports
======================================================================
[2024-04-08T21:00:52.841Z] Balance change detected on node: https://nd-094-012-520.p2pify.com
[2024-04-08T21:00:52.841Z] New balance: 1104846848 lamports
======================================================================
[2024-04-08T21:00:54.139Z] Balance change detected on node: https://nd-094-012-520.p2pify.com
[2024-04-08T21:00:54.139Z] New balance: 1104841848 lamports
======================================================================

Conclusion

To wrap up this tutorial, we’ve explored the world of leveraging multiple RPC endpoints across different geographical locations to enhance the reliability and performance of Solana-based applications. We’ve demonstrated a pragmatic approach to optimizing blockchain interactions for a seamless user experience by implementing a real-time connection pool to listen for account balance changes. This method minimizes latency, mitigates the risk of single points of failure, and ensures your application remains resilient and responsive under varying network conditions.

Ake

Ake Director of Developer Experience @ Chainstack
Talk to me all things Web3
20 years in technology | 8+ years in Web3 full time years experience
Last modified on July 4, 2026