How We Cut Base58 CPU Usage from 20% to 1–2% in Our Solana RPC Stack

We built a Go Base58 codec optimized for Solana, offering 10x performance gains.

CloakdDev avatar CloakdDev · Technical Founder at FluxRPC · August 18, 2026 · 6 min read

Intro

Applications on Solana use Base58 encoding extensively to represent data. Public keys, signatures, transactions, and RPC responses typically use Base58. Production profiling at FluxRPC showed that around 20% of total CPU time was spent on encoding or decoding. After applying our optimized codec, we saw the time spent drop to 1–2% at most, in addition to significant improvements to latency.

Our optimizations improve upon the fixed-size cases for Solana data (e.g. 32-byte public keys). We applied these improvements to Firedancer's C implementation, adding AVX2 kernels and reducing unnecessary work throughout its existing matrix-based conversion. This resulted in performance gains to the common fixed-size cases, and even larger gains for the larger arbitrary-size cases.

Base58 Current State

Base58 conversion is a radix conversion between base 256 and base 58. Unlike Base64, there is no convenient bit boundary between the representations, so implementations commonly use repeated long division to extract Base58 digits.

Solana presents two different performance problems:

  • Fixed-size values: Public keys and signatures are fixed at 32 and 64 bytes, allowing conversion to be optimized for those common cases.
  • Arbitrary-size values: RPC responses containing account data can be much larger (megabytes), making byte-at-a-time long division particularly expensive.

A practical implementation needs to have good scaling characteristics as well as fixed-size optimizations.

Firedancer already handles the fixed-size case using precomputed conversion matrices. Binary limbs are multiplied against precomputed radix-conversion constants and normalized into groups of Base58 digits, with the inverse transformation used for decode. This avoids generic long division for the most common 32- and 64-byte values.

Our Go starting point was solana-go/base58, which was based on this approach. We focused on improving both these fixed-size paths and the arbitrary-length case.

Improvements Made

Go

For 32- and 64-byte values, fluxrpc/base58 uses dedicated conversion paths based around precomputed radix matrices. Rather than repeatedly dividing an arbitrary-sized integer by 58, the input is split into fixed-width limbs and transformed into larger Base58 radix groups:

  • Precomputed limb shape: The implementation works with powers such as 58⁵, representing multiple Base58 digits in each intermediate limb. With known 32- and 64-byte boundaries, the number and shape of those limbs is fixed, allowing most of the conversion work to be done ahead of time.
  • AVX2 where available: On amd64, the hot fixed-size paths use AVX2 assembly with runtime CPUID detection, with scalar assembly and pure-Go fallbacks where AVX2 is unavailable.
  • No heap allocations: Decode and append-based APIs avoid heap allocations, while append encoding allows RPC response builders to encode directly into an existing output buffer.

The more significant algorithmic change for large values is the arbitrary-length codec. Traditional Base58 implementations commonly perform long division byte-by-byte, repeatedly walking large portions of the input for each stage of the conversion. This scales poorly as the payload grows.

fluxrpc/base58 instead uses a limb-based codec, processing substantially more of the integer per arithmetic operation. The reduction in arithmetic work becomes increasingly significant as input size increases, which is why the difference between implementations grows from a few times faster on small values to tens of times faster on larger payloads.

This matters for Solana RPC beyond synthetic large-input benchmarks. Base58 can be used for account data and other RPC payloads substantially larger than public keys and signatures, including account representations reaching into the megabytes. At those sizes, the scaling behaviour of the generic codec can dominate the cost of serialization or ingestion.

C / Firedancer

We applied the fixed-size optimization work to Firedancer's C implementation. The existing implementation already used precomputed matrices for 32- and 64-byte conversions, so the focus was reducing the cost of those matrix operations and the surrounding normalization:

  • AVX2 matrix-vector kernels: Four parallel unsigned 32×32 → 64-bit multiplications let multiple conversion columns be accumulated simultaneously rather than processed sequentially.
  • Skipping empty blocks: The matrices themselves also contain structure that can be exploited. The 64-byte decode table is triangular in four-column blocks, allowing blocks containing only zeroes to be skipped entirely — reducing the relevant matrix work from 72 vector multiplies to 44.
  • Reusing the 32-byte kernel: The 64-byte encode matrix contains a section equivalent to the 32-byte matrix shifted by nine columns. Rather than independently processing those rows, the optimized implementation reuses the 32-byte encode kernel for the lower half of the 64-byte input.
  • Building groups directly: Decode now constructs Base58⁵ groups directly from the bounded input, rather than first materializing a padded array of individual Base58 digits. Character validation is performed during the same grouping operation, and maximum-length inputs use specialized heads where their shape is known ahead of time.
  • Less memory traffic: Carry propagation during AVX encode and normalization during decode are kept in registers where possible, and decode emits two binary limbs per store.

Benchmarks

Go

The Go benchmarks compare fluxrpc/base58 against solana-foundation/solana-go using the same machine, deterministic input corpus and benchmark binary.

Operationsolana-gofluxrpc/base58Speedup
Encode 16 B311 ns91.6 ns3.40×
Decode 16 B277 ns63.2 ns4.38×
Encode 32 B125 ns65.2 ns1.92×
Decode 32 B111 ns63.8 ns1.75×
Encode 64 B474 ns121 ns3.91×
Decode 64 B392 ns112 ns3.50×
Encode 100 B8.74 µs578 ns15.13×
Decode 100 B7.96 µs312 ns25.49×
Encode 1 KB845 µs35.9 µs23.53×
Decode 1 KB719 µs10.6 µs67.55×

The increasing speedup with input size is important for RPC workloads. The generic 32-byte decode is 1.75× faster, while at 1 KB the limb-based decoder is 67.55× faster. The difference comes from avoiding the scaling behaviour of byte-at-a-time long division rather than simply optimizing the same loop.

For known Solana types, the dedicated APIs remove additional dispatch, copying and allocation overhead:

OperationTimeAllocations
Encode32~57.2 ns1
AppendEncode32~32.9 ns0
Decode32~30.2 ns0
Encode64~107.6 ns1
AppendEncode64~73.8 ns0
Decode64~59.9 ns0

C / Firedancer

The Firedancer benchmarks compare the optimized implementation against the latest main, built with identical Haswell flags and measured through the same benchmark binary.

OperationFiredancer mainOptimizedSpeedupReduction
Encode 32 B54.351 ns36.067 ns1.52×34.1%
Encode 64 B133.977 ns77.277 ns1.73×42.1%
Decode 32 B83.894 ns38.171 ns2.21×54.7%
Decode 64 B187.377 ns79.993 ns2.35×57.4%

The largest gains are on decode, with the 32-byte path reduced by 54.7% and the 64-byte path by 57.4%, despite Firedancer already using specialized fixed-size radix conversion.

Production Results

This work originated from Base58 consistently accounting for around 20% of total CPU time in our production RPC stack. After deploying the optimized implementation, Base58 encode/decode now accounts for approximately 1–2% of CPU at most.

The production workload combines a very high volume of 32- and 64-byte operations with much larger encoded RPC payloads. The fixed-size paths reduce the cost of the most frequent operations, while the limb-based generic codec prevents larger Base58 payloads from scaling into disproportionately expensive conversions.

We also observed a substantial reduction in latency for Base58-heavy RPC methods. This is visible in Solana's public RPC performance measurements, where FluxRPC ranks #1 across many RPC response categories, depending on geographic location.

Source code

The implementations and benchmark methodology are available here: