Optimizing ethers for low-latency transactions on rollups

Learn how you can optimize ethers to perform low-latency transactions on rollups.

Optimizing ethers for low-latency transactions on rollups

The ethers library is widely used, but also old, and as a result isn’t optimized for low latency interactions with new EVM technologies like rollups. We investigated an interesting issue here, related to transaction confirmation times on an Arbitrum L3.

Our sequencer is located in North America, and from San Francisco, the performance is great. But our users in Asia were not seeing great performance. So naturally, we thought about running an L3 RPC node in Asia for users over there. This should improve performance for reading data, but when writing data, the node must send and confirm that transaction with the sequencer, which is in North America. Will putting a node in Asia help?

We ran some tests which make a txn, and then wait for it to be confirmed. We saw that in the write case, performance was actually worse! Naively setting up a replica in Asia actually makes the network slower for Asian users. Take a look at some numbers below to understand.

Simple tests using ethers.js 5.7.2

From location, to location Median time
San Francisco → North America 0.59s
Asia → North America 2.45s
Asia → Asia 4.36s

Initially, this seemed like an issue with the rollup replica node and how it talks to the sequencer. Maybe the replica has to wait for the sequencer in North America to confirm the txn?

But we actually have 2 versions of this test that we can run: a simple one, that uses ethers.js 5.7.2 to do everything. And a complex one, that manually estimates gas, gets the txn nonce, gets the gas price, and then manually loops over the transaction hash until it’s confirmed. There shouldn’t be a meaningful difference between the two methods, and we typically see that the complex case is just a little bit faster, probably due to a tighter polling loop on checking that the txn was confirmed. But look at the Asia → Asia results below for the complex case.

Simple vs complex tests

From location, to location Simple case (median time) Complex case (median time)
San Francisco → North America 0.59s 0.56s
Asia → North America 2.45s 2.2s
Asia → Asia 4.36s 0.44s

It’s really fast! This would indicate that something in ethers.js is slow, with respect to confirming the transaction. So we dug into the ethers.js source code.

Ethers.js has a default polling time of 4s. But, it initially checks if the txn is confirmed, and can skip the 4s wait if the txn is already confirmed. You can see this in the _waitForTransaction function here.

So, we tried setting the polling interval to 100ms, which is the same amount of time we use in the complex test for manual polling. The median time for the Asia → Asia test became 0.56s!

From location, to location Simple case (polling at default 4s) Simple case (polling at 100ms)
San Francisco → North America 0.59s 0.59s
Asia → North America 2.45s 2.89s
Asia → Asia 4.36s 0.56s

The change needed was here.

const provider = new ethersv5.providers.JsonRpcProvider(rpcUrl);
provider.pollingInterval = 100;
const wallet = new ethersv5.Wallet(privateKey, provider);
return wallet;

The code used to perform these tests is in this repo https://github.com/LIT-Protocol/SyncTest/tree/main and makes 100 txns sequentially, and then reports various metrics about them.

Conclusion

If you’re optimizing for low latency, it’s really important to set the pollingInterval in ethers. Adding local RPC nodes can help with latency, but only if you set the pollingInterval.

Happy building!