შინაარსზე გადასვლა
operations21 ოქტომბერი, 2026 · 7 min read

Cutting blockchain RPC costs: how we went from 17 million to about 5.6 million calls a month

We sized our RPC plan for 2–5 million calls a month and measured 17 million. Where the calls came from, what removed them, and what each change cost in latency.

Kostiantyn Boiko, Blockchain Engineer

English original

When we sized an RPC plan for EX5, the estimate was 2 to 5 million calls a month. Then we measured. The gateway that sits between our services and the chain endpoints counted about 17 million calls a month over 7.3 days, and a packet capture agreed.

Nobody had added a feature that needed four times the traffic. The calls came from loops: a balance job, block scanners and a safety poller, each sensible on its own. This is how we found them, what we changed on 20 October 2026, and the two changes that did nothing.

Count before you cut

All chain traffic in our stack goes through one gateway with RPC pools and failover per chain, which made counting possible. Its counters gave us totals per chain; a one-minute capture gave us methods. The baseline:

Chain Calls per month Where they came from
Ethereum ~4.6M 3.8M of it eth_call balanceOf, from a balance job
Avalanche ~2.6–4.3M one eth_getBlockByNumber per block
Solana ~2.4M a deposit safety poller and slot queries
TRON ~2.1M head checks and one call per block
XRP Ledger ~1.3M normal ledger scanning
Not attributed ~2.3–4.0M the gap between the chain sum and the gateway total

Two lessons came out of the counting itself:

  • timeout 60 tcpdump ... | grep printed nothing for us, because the pipe’s buffer is lost when timeout sends SIGTERM. Write the capture to a file with tcpdump -U -w, then read it back with -r.
  • The gateway checks the health of each endpoint every 15 seconds. That is about 173,000 calls a month per endpoint before your application sends anything. Count it when you compare paid plans.

The balance job that never slept

The largest single source was a job that refreshes wallet balances for the admin views. It was written as a loop with a fixed 60-second sleep, with no setting or environment variable to change it. Each round read the balance of every wallet for every currency and took about 68 seconds, so in practice it never stopped.

It also read the same thing several times. One address can appear in more than one wallet row (hot, deposit, fee), and every row triggered its own read. On Ethereum, three rows on one address meant 88 reads for 43 unique balances. Wallet rows with no node configured first threw an error and then fell back to reading the node anyway, which filled the logs at about 43 errors a minute.

What we changed:

  • The interval is a setting, now 15 minutes. The default stays at 60 seconds, so nothing changes for a deployment that does not set it.
  • Each round reads every (chain, address, currency) once and shares a successful result between the rows that point at it. A failed read is never shared.
  • Rows without a node configured read directly, without the error, and back off when they keep failing.

The cost is that balances in the admin views can be up to 15 minutes old. We checked what depends on them before changing anything: withdrawals use the live balance, and no alert reads the cached value.

Before we shipped it, we ran the old and new code side by side over 1,000 scenarios, three rounds each. 58,953 values came out identical. The only differences were 138 balances that the old code showed as N/A and the new one showed correctly.

Those 138 led to a bug that had nothing to do with volume. The TRC-20 balance call put the base58 address (the one starting with “T”) straight into the ABI encoding. The upstream node rejected it as invalid hex and the bridge returned 502, so that balance had always shown as N/A. A TRON address is 20 bytes of hex underneath; encoding it that way fixed it, and the value now reads correctly in production.

Head polling on a 3-second chain

TRON produces a block about every 3 seconds. Our block scanner asked for the latest block number on every loop and only slept once it had caught up, so it made roughly one head call per block. Then it fetched each block with its own call. Each of those added up to about 0.86 million calls a month.

The fix lives in the TRON bridge, so the scanner did not change:

  • The latest block number is served from a cache for up to 30 seconds.
  • On a cache miss, one getblockbylimitnext call fetches up to 20 consecutive blocks, never past the cached head.
  • The bridge only trusts a run of blocks that starts at the requested block and has no gaps. Anything else falls back to the old one-block call.

We checked that all three of our upstream providers support the range call and that the blocks it returns are byte-identical to single-block fetches. In an offline simulation of the scanner, 30 minutes went from 967 upstream calls to 117, with no block skipped. In production, TRON went from 49 to 4 calls a minute.

The cost: a deposit can be noticed up to 30 seconds later. TRON confirmations already take about a minute, so customers do not see the difference.

The Solana poller we could not just slow down

Solana deposits had a second line of defence: a poller calling getSignaturesForAddress for each deposit wallet every 10 seconds, about 1.8 million calls a month. The 10 seconds was not arbitrary. The poller had to finish before the block scanner’s window of about 26 seconds passed, or a deposit could slip between the two. Raising the interval alone would have risked missed deposits.

So we tied the two together. When the scanner asks for the latest finalized slot, the bridge answers with the slot up to which the poller has checked every wallet, so the scanner can never run ahead of the poller. With that guarantee the poller runs every 60 seconds.

One behaviour almost caught us. If the node does not know the signature you pass as before, Solana returns an empty page, which looks exactly like the end of the history. Paging without a floor in that case loses deposits, and a negative-control test showed it doing so. Every page now carries a minimum context slot and retries; the first round looks back about an hour.

We tested it with 5 random seeds of 400 rounds each, with 15% of requests failing, lagging nodes and deposit bursts. No signature was missed.

The change also explained most of the traffic nobody could attribute. The Solana scanner had been spinning on getSlot about 100 times a minute: the bridge answered its block requests locally, so the scanner never had a reason to sleep. The guard answers that call too. Solana went from 55 to 12 calls a minute.

What did not work

  • A head cache on Avalanche. We added the same 30-second cache and measured 61 calls a minute before and 62 after. Almost all Avalanche traffic is the per-block fetch, which a head cache does not touch. The plan had warned that the best case was about half; the result was zero.
  • Turning off deposits on a quiet chain. It does not reduce RPC at all: the scanner still fetches every block, because it also confirms withdrawals. Only switching off the chain does, and that means withdrawals too.
  • A head cache on Ethereum. With 12-second blocks the gain is small, and we left it off.

If you do switch a chain off, reset its scan height to roughly the current head minus 100 blocks before switching it back on. Otherwise the scanner starts where it stopped, rescans months of blocks, and sends you the same RPC bill in one go.

The result, and how sure we are

Chain Calls per minute, before After
Ethereum 107 20
Solana 55 12
TRON 49 4
XRP Ledger 30 31
Avalanche 61 62
Total 302 129

That is a like-for-like one-minute sample taken just before and just after the switch: 57% fewer calls. 129 a minute is about 5.6 million a month. A second sample in the same hour read about 100 a minute plus the 15-minute balance round, close to 4.6 million.

Both are short samples, and the 7-day baseline was taken differently, so treat “17 million to about 5.6 million” as a first reading. We repeat the 7-day measurement on 27 October and will update this article with it. One number is already certain: errors from the balance job went from about 43 a minute to zero.

We rolled the changes out one at a time over 11 minutes, each behind its own setting and each with its own rollback. Only the balance job’s service got new code; the deposit and withdrawal daemons kept running the image they had.

If you are sizing an RPC plan

Do not estimate from transaction counts. RPC volume grows with chains, blocks and wallets, and barely with customers. A quiet exchange with nine chains can make more calls than a busy one with three.

Before buying a bigger plan, look at three things: jobs that read balances on a timer, scanners that ask for the head on every loop, and pollers whose interval is a safety condition rather than a choice. In our case those three were most of the bill.

The gateway, RPC pools and chain bridges described here are part of Custody. For the on-chain side of the same cost question, see how to cut TRON energy costs for USDT transfers.

If you want us to look at your chain traffic the same way, contact sales.

დაჯავშნეთ 30-წუთიანი შეფასების ზარი

Crypto exchange, payments, wallet, custody and token launch software, already running in production. Run it on your own servers, license the source code, or let us host it under your brand.