Resilient Json Rpc Provider: Build a reliable dApp with multiple unreliable rcp urls
The Problem
As developers and serial builders, we often have random project ideas pop up in our heads, some worth building, while others aren’t.
For the ones worth pursuing, you always want to first get an MVP rolled out and get user feedback. More often than not, you’re working on a budget, so you’ll gladly opt for a free plan whenever possible.
Although there are free public rpc urls, and most RPC providers offer free plans, the obvious problem is that these free rpcs are heavily rate-limited and usually only useful for development. Shipping your dApp to real users with a free-plan RPC URL isn’t sometheing you want to do, unless you want to bombard your users with error 429 or -32005 responses.
The solution
Since there are a lot of free public RPCs, and most node providers (like Alchemy, Infura, Moralis, etc.) offer free tiers, what if you could combine multiple free and rate-limited URLs and build a reliable dApp at no cost?
To achieve this, you need to create a class that inherits from Ethers’ StaticJsonRpcProvider
. Let’s call it ResilientRpcProvider
.
The main idea here is to override the send
method of the inherited StaticJsonRpcProvider
class so that it can:
- Avoid rate limit issues by distributing the load across your RPC URLs in a round robin manner.
- Minimize failures by recursively repeating the request with the next provider in the list until one succeeds, or return the last error if all providers fail.
It’s simple. In the class, you want to have a list of StaticJsonRpcProvider
instances, each connected to a different RPC URL. So every time a request is made through the provider, it is sent recursively through each of the providers in the list until one finally succeeds, or all fail. Additionally, since you're trying to avoid rate-limit issues, you can write this class in such a way that it distributes all of your calls among all the RPC URLs in a round robin manner.
import { StaticJsonRpcProvider } from "@ethersproject/providers";
/**
* ResilientRpcProvider
*
* A modified version of StaticJsonRpcProvider that handles failover between multiple RPC endpoints
*/
export class ResilientRpcProvider extends StaticJsonRpcProvider {
/**
* Array of providers to be used for retries
*/
providers: StaticJsonRpcProvider[];
/**
* Index of the currently active provider in the providers array.
*/
activeProviderIndex: number;
/**
* Error thrown by the last failed provider
*/
error: any;
/**
* Flag indicating if logging is enabled.
* If true, logs will be output to the console when failover occurs.
*/
loggingEnabled: boolean;
/**
* @param rpcUrls Array of RPC urls to be used to initialize providers
* @param chainId Chain Id of the network
* @param loggingEnabled Flag indicating if logging is enabled.
*/
constructor(rpcUrls: string[], chainId: number, loggingEnabled = false) {
// Initialize the super class with the first RPC url.
super(rpcUrls[0], chainId);
// Create an array of providers off of the rpcUrls
this.providers = rpcUrls.map(
(url) => new StaticJsonRpcProvider(url, chainId)
);
// Start off as -1 and will be incremented to 0 just before the first call is made
this.activeProviderIndex = -1;
this.loggingEnabled = loggingEnabled;
}
/**
* Overrides the send method of the super class to provide retry functionality
*
* This send function is the highlight of this class.
* It takes advantage of the parent class's send method, but provides retry
* functionality in case of failures.
*
* If the retryCount is equal to the number of providers, it means that all providers have failed.
* In that case, it will throw the last error it encountered.
*
* @param method RPC method to execute
* @param params Parameters of the RPC method
* @param retryCount Number of times the call had been retried (default: 0)
* @returns The result of the RPC method
*/
async send(
method: string,
params: Array<any>,
retryCount = 0
): Promise<any> {
this.validateRetryAttempt(retryCount);
try {
const provider = this.getNextProvider();
return await provider.send(method, params);
} catch (error) {
this.error = error;
if (this.loggingEnabled) {
console.debug(
`provider ${
this.providers[this.activeProviderIndex].connection.url
} Failed. Trying the next provider in the list`
);
}
return this.send(method, params, retryCount + 1);
}
}
/**
* Gets the next provider in the list of providers
*
* @returns The next provider
*/
private getNextProvider(): StaticJsonRpcProvider {
this.activeProviderIndex = (this.activeProviderIndex + 1) % this.providers.length;
return this.providers[this.activeProviderIndex];
}
/**
* Validates the retry count and throws the last error saved if the retryCount equals or exceeds the number of providers.
*
* @param {number} retryCount - The number of times the request had been retried
*/
private validateRetryAttempt(retryCount: number): void {
if (retryCount >= this.providers.length) {
const error = this.error;
this.error = undefined;
throw new Error(error);
}
return void 0;
}
}
How it works
The ResilientRpcProvider
class is a custom implementation that extends the StaticJsonRpcProvider
from the @ethersproject/providers
library. Its main purpose is to provide a reliable way to interact with multiple RPC endpoints, in a way that ensure your dApp remains functional even when some of the providers are unreliable or rate-limited.
At its core, the class maintains a list of StaticJsonRpcProvider
instances, each connected to a different RPC URL. Whenever a request is made through the send
method, it iterates through the list of providers, sending the request to each one until a successful response is received or all providers have failed.
If a provider fails to respond or encounters an error, the class takes note of the error and moves on to the next provider in the list, recursively calling the send
method until a successful response is obtained. This approach ensures that your dApp can continue functioning even if one or more RPC providers experience downtime or rate-limiting issues.
Additionally, the class implements a simple load-balancing mechanism by cycling through the list of providers for each successive request. This helps distribute the load across all available RPC URLs, further reducing the likelihood of hitting rate limits.
If all providers in the list fail to respond successfully, the class throws an error with the last encountered error message. This error can be caught and handled appropriately within your application code.
Usage
Using the provider in your application is quite simple — just pass in an array of RPC URLs and the network’s chain ID.
/**
* Array of heavily rate-limited public BSC chain RPC urls (curated from https://chainlist.org).
*/
const rpcUrls = [
"https://rpc.ankr.com/bsc",
"https://bscrpc.com",
"https://bsc-dataseed3.defibit.io",
"https://bsc-dataseed4.defibit.io",
"https://bsc-dataseed3.ninicoin.io",
"https://bsc.rpc.blxrbdn.com",
];
/**
* A ResilientRpcProvider instance with the above RPC urls
* and the BSC chain id (56).
*/
export const provider = new ResilientRpcProvider(rpcUrls, 56);
You can then use the provider
instance like any other Ethers provider, with the added benefit of failover and load balancing across multiple RPC endpoints.
Pros and Cons
While the ResilientRpcProvider
offers a reliable way to interact with the blockchain using multiple RPC endpoints, it's important to understand both the advantages and disadvantages of this approach.
Advantages
- Increased Reliability: By utilizing multiple RPC providers, your dApp becomes more resilient to individual provider failures or rate-limiting issues. If one provider goes down or becomes unresponsive, the requests are automatically routed to the next available provider, ensuring your application remains functional.
- Cost-Effective: By leveraging free heavily rate-limited RPC providers, you can build and deploy your dApp without incurring the costs associated with paid RPC services. This approach is especially beneficial for early-stage projects or developers operating on a tight budget.
- Load Balancing: The provider distributes requests across all available RPC URLs, helping to distribute the load and potentially avoid rate-limiting issues with any single provider.
Disadvantages
- Increased Latency: Routing requests through multiple providers can potentially increase the overall latency of your application. Each failed request and subsequent retry introduces additional delays, which could impact time-sensitive operations.
- Inconsistent Responses: Different RPC providers may return slightly different responses or handle edge cases differently. While this is unlikely for most standard operations, it’s something to be aware of, especially when dealing with complex or uncommon scenarios.
- Provider Maintenance: As your application grows and becomes more reliant on this approach, you’ll need to actively maintain and update the list of RPC providers. Providers may change their URLs, rate-limiting policies, or even shut down entirely, requiring you to adapt your configuration accordingly.
As with any architectural decision, it’s important to carefully weigh the pros and cons and determine if this approach aligns with the specific needs and constraints of your project.
…In Conclusion
The ResilientRpcProvider
is a very good approach for builders looking to ship reliable dApps on a budget. By combining multiple free rate-limited RPC endpoints with failover and load balancing, this solution ensures your application remains functional even when individual providers fail.
This approach offers a cost-effective way to enhance your dApp’s reliability and availability. However, it’s important to carefully weigh the potential trade-offs.
Ultimately, this approach empowers you to build robust and resilient dApps, without breaking the bank or compromising on user experience.
And that does it for this article.
Is this something you’d like to try out in your next project?
Don’t forget to drop a clap and share if you find it helpful. Also, do well to drop a comment if you have any questions or would like to discuss this topic further.