An extremely fast, dependency-free Base62 encoder and decoder for .NET.
- Extremely fast (see Benchmarks) encoding and decoding compared to other libraries thanks to:
- Block-based encoding algorithm that runs in
$O(n)$ time instead of the usual$O(n^2)$ time. - Span-based character and UTF-8 APIs.
- Allocation-free destination-based encoding and decoding.
- A single exact result allocation for the string and array convenience methods.
- Block-based encoding algorithm that runs in
- Streaming APIs.
- No runtime dependencies.
- Supports .NET 8, .NET 9, and .NET 10.
Base62 is well suited for user-facing identifiers, links, and tokens:
- No special characters: Base62 is entirely alphanumeric. It is naturally URL-safe. Double-clicking selects the entire string.
- Compact: Base62 represents binary data much more efficiently than hexadecimal while remaining entirely alphanumeric. For example, eight bytes use 11 Base62 characters instead of 16 hexadecimal characters.
dotnet add package Spotflow.Base62using Spotflow.Base62;
ReadOnlySpan<byte> data = stackalloc byte[] { 1, 2, 3, 4, 5 };
string encoded = Base62.EncodeToString(data);
byte[] decoded = Base62.DecodeFromChars(encoded);Use destination-based APIs to avoid allocations:
ReadOnlySpan<byte> data = stackalloc byte[] { 1, 2, 3, 4, 5 };
Span<char> encoded = stackalloc char[Base62.GetEncodedLength(data.Length)];
int charsWritten = Base62.EncodeToChars(data, encoded);
Span<byte> decoded = stackalloc byte[Base62.GetDecodedLength(charsWritten)];
int bytesWritten = Base62.DecodeFromChars(encoded, decoded);Equivalent EncodeToUtf8 and DecodeFromUtf8 overloads operate on ASCII-compatible UTF-8 bytes. Try overloads avoid exceptions for malformed data and small destinations. Advanced overloads return OperationStatus together with consumed and written counts for blockwise processing.
Convert the string to bytes using an appropriate text encoding before encoding it as Base62:
using System.Text;
using Spotflow.Base62;
const string text = "Hello, world!";
Span<byte> utf8 = stackalloc byte[Encoding.UTF8.GetByteCount(text)];
Encoding.UTF8.GetBytes(text, utf8);
string encoded = Base62.EncodeToString(utf8);Use the OperationStatus overloads to process data without loading the entire input into memory. Preserve any unconsumed bytes between reads so that Base62 blocks remain contiguous:
using System.Buffers;
using Spotflow.Base62;
await using FileStream source = File.OpenRead("input.bin");
await using FileStream destination = File.Create("output.base62");
const int BufferSize = 8 * 1024;
byte[] inputBuffer = new byte[BufferSize];
byte[] outputBuffer = new byte[Base62.GetEncodedLength(BufferSize)];
int buffered = 0;
while (true)
{
int bytesRead = await source.ReadAsync(inputBuffer.AsMemory(buffered));
bool isFinalBlock = bytesRead == 0;
int available = buffered + bytesRead;
OperationStatus status = Base62.EncodeToUtf8(
inputBuffer.AsSpan(0, available),
outputBuffer,
out int bytesConsumed,
out int bytesWritten,
isFinalBlock);
await destination.WriteAsync(outputBuffer.AsMemory(0, bytesWritten));
inputBuffer.AsSpan(bytesConsumed, available - bytesConsumed)
.CopyTo(inputBuffer);
buffered = available - bytesConsumed;
if (isFinalBlock && status == OperationStatus.Done)
{
break;
}
if (status is not (OperationStatus.Done or OperationStatus.NeedMoreData))
{
throw new InvalidOperationException($"Unexpected status: {status}");
}
}The same pattern works for decoding with Base62.DecodeFromUtf8; decoded input is processed in 11-character blocks instead of 8-byte blocks.
Important
Base62 has no single standard binary encoding. This library guarantees a single deterministic encoding for any input data and a single deterministic decoding for any Base62 string generated by this library. It is not guaranteed to decode Base62 strings generated by other libraries as they may use different alphabets, block sizes, etc.
The alphabet is 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz. Data is split into blocks of up to eight bytes. Each block is interpreted as a little-endian unsigned integer, and Base62 digits are written least-significant first. Full blocks contain 11 characters; the final partial block has a dynamic width based on the number of input bytes. The encoding is deterministic and preserves leading and trailing zero bytes.
Examples:
| Input (hex) | Encoded value |
|---|---|
01 |
10 |
FFFF |
13H |
FFFFFFFFFFFFFFFF |
FYHA61aHgyL |
010203040506070809 |
zipgg0YFjg090 |
| Method | Condition | Exception |
|---|---|---|
Base62.DecodeFromChars, Base62.DecodeFromUtf8 |
The input contains an invalid character, block length, or block value | FormatException |
Base62.EncodeToChars, Base62.EncodeToUtf8, destination-based decode methods |
The output buffer is too small | ArgumentException |
Base62.GetDecodedLength |
The encoded length cannot represent valid data | FormatException |
| Length methods | The supplied length is negative | ArgumentOutOfRangeException |
Base62.TryGetDecodedLength returns false for an encoded length that cannot represent valid data. The other Try methods return false instead of throwing for malformed data or insufficient destination space. On failure, their written count reports the successfully processed prefix and the destination may be partially modified. The OperationStatus overloads distinguish InvalidData, DestinationTooSmall, and NeedMoreData.
Run the encoding and decoding benchmarks against a supported framework:
dotnet run --project benchmarks/Spotflow.Base62.Benchmarks -c Release -f net10.0The benchmarks compare Spotflow.Base62 with SimpleBase and ghost1face/base62 using each library's binary API. Spotflow reuses destination buffers for encoding and decoding. SimpleBase returns a string when encoding, while ghost1face returns Base62 digit values as a byte[], so allocation results should be interpreted with those API differences in mind. Destination buffers are reused whenever an API supports them.
Spotflow.Base62 is consistently faster and offers zero allocations for destination-based APIs. The difference is most pronounced for larger inputs thanks to the linear-time block-based algorithm (e.g. 4,000x faster for 4 KiB inputs).
Each result shows mean time / ratio to Spotflow / allocated memory.
| Input | Spotflow.Base62 | SimpleBase | ghost1face/base62 |
|---|---|---|---|
| 64 B | 96.33 ns / 1.00x / 0 B | 5.664 us / 58.81x / 200 B | 7.566 us / 78.55x / 400 B |
| 128 B | 194.55 ns / 1.00x / 0 B | 28.774 us / 147.90x / 368 B | 31.418 us / 161.50x / 704 B |
| 1024 B | 1.652 us / 1.00x / 0 B | 1.840 ms / 1,118.81x / 5,560 B | 2.353 ms / 1,430.51x / 4,904 B |
| 4096 B | 7.741 us / 1.00x / 0 B | 32.646 ms / 4,218.55x / 22,072 B | 34.194 ms / 4,418.60x / 19,308 B |
| Input | Spotflow.Base62 | SimpleBase | ghost1face/base62 |
|---|---|---|---|
| 64 B | 77.76 ns / 1.00x / 0 B | 3.919 us / 50.41x / 0 B | 7.602 us / 97.79x / 424 B |
| 128 B | 148.73 ns / 1.00x / 0 B | 12.939 us / 87.01x / 0 B | 32.664 us / 219.66x / 752 B |
| 1024 B | 908.78 ns / 1.00x / 0 B | 676.44 us / 744.92x / 0 B | 1.693 ms / 1,864.66x / 5,256 B |
| 4096 B | 3.643 us / 1.00x / 0 B | 10.591 ms / 2,908.78x / 0 B | 27.814 ms / 7,638.59x / 20,717 B |
See the contribution guidelines before opening a pull request. Security issues should be reported according to the security policy.
This project is licensed under the MIT license.