Z85 and RFC 1924 Base85 Field Guide: When Four Bytes, Five Characters, and Two Alphabets Decide Whether Your Payload Survives the Wire

Base85 encoding poster: four bytes in, five characters out

The Base85 family packs four bytes into five printable characters, and the alphabet you choose decides whether the result decodes correctly or silently produces garbage. Z85 is the ZeroMQ flavour with a fixed 85-character set; RFC 1924 Base85 is the Ascii85-adjacent standard with a different 85 characters in a different order. Both encode the same bytes and produce completely different strings. A single mis-selected alphabet turns a valid payload into unreadable noise, and because the output is still well-formed printable text, nothing downstream complains. The tools in this guide make that choice explicit and inspectable instead of leaving it to a library default.

The 4-to-5 block is the whole contract

Every Base85 variant works on the same primitive: take four bytes, treat them as one big-endian 32-bit unsigned integer, and write that integer in base 85 padded to exactly five digits. Four bytes is 2^32 possible values; five base-85 digits span 85^5, which is 4.437e9, comfortably larger than 2^32 = 4.295e9. That headroom is why the mapping is lossless in both directions with no partial digit, no carry, and no ambiguity.

The consequence is a hard structural rule that shows up everywhere in this guide. A valid Z85 string always has a length that is a multiple of five. There is no padding character, because the block length is fixed and every 85^5 value that a 32-bit integer can take has its own unique five-digit form. If your payload is not a multiple of four bytes, the leftover one, two, or three bytes cannot be encoded as a complete block.

Working through the actual arithmetic makes the trade-off concrete. Encoding 00010203 as Z85 produces 009c6. The manifest’s own worked example asserts exactly that value, and you can reproduce it by hand: the bytes 00 01 02 03 form the integer 66051, and 66051 written as five base-85 digits is 009c6. Feeding 009c6 back through the RFC 1924 decoder returns 00010203, closing the loop.

Z85 and RFC 1924 share a shape, not an alphabet

The trap most people hit is assuming there is one Base85. There are at least two incompatible ones in common use, and they diverge at every character position.

Same four bytes encode to two different strings under Z85 and RFC 1924 alphabets

The ZeroMQ Z85 alphabet interleaves digits, lowercase, uppercase, then a run of punctuation. The RFC 1924 alphabet uses digits, then uppercase, then lowercase, then a different punctuation run. The letter A sits at index 36 in Z85 but index 10 in RFC 1924. The letter a is the mirror image: index 10 in Z85, index 36 in RFC 1924. This is not a cosmetic reordering. It is a different numeral system.

Take the four bytes DE AD BE EF. Encoded as Z85 the result is ?MsJX. Encoded as RFC 1924 the very same four bytes become -mSjx. Five characters either way, both perfectly valid, and neither will decode under the other’s alphabet. If you ever see ?MsJX in a ZeroMQ wire capture and try to decode it as RFC 1924, you get four bytes that are almost certainly not what the sender meant.

Running the same block through both alphabets is the fastest way to see what a wrong-alphabet bug actually looks like. A good round-trip tool will show you both strings side by side and let you decode each one, which is exactly what the Z85 RFC 1924 Base85 Stream Encoder Decoder does when you switch its variant selector.

Where the density win actually comes from

Base85 earns its place in a protocol because of how much smaller it makes a payload than the alternatives. Working the numbers per byte:

Characters per byte compared: hex 2.00, Base64 1.33, Base85 1.25
  • Hex renders every byte as two characters, so two characters per byte. It is readable and utterly wasteful.
  • Base64 uses six bits per character, costing 4 characters for every 3 bytes, or 1.333 characters per byte.
  • Base85 packs more than six bits into each character, costing exactly 5 characters for every 4 bytes, or 1.25 characters per byte.

Against hex, that is a 37.5 percent reduction. Against Base64 it is about six percent, which is real but modest. The honest framing is that Base85 is a hex-replacement, not a Base64 killer. In ZeroMQ’s CURVE and WireGuard-adjacent designs, the win is that a fixed-length binary key field becomes a short, printable, log-safe, copy-pasteable token with no padding characters to confuse downstream parsers.

If you are converting between the text encodings rather than packing binary keys, the comparison matters more than the density. A Base64 converter is the right tool for interchange text; a URL-safe variant like Base64url encoder exists because + and / need escaping in a URL. Base85 sidesteps that whole problem, since its punctuation set was chosen to be copy-pasteable without a second escaping layer.

Why the partial-block tail is the real failure mode

Every operational problem with Base85 lives in the last few bytes of a stream, not in the middle. A Z85 payload of length 23 has four complete blocks and three leftover bytes that cannot be represented.

Option one: pad with a documented sentinel. Some protocols append zero bytes and record the real length separately. This is what makes a fixed-length token possible, but it costs a side channel.

Option two: use a stream variant. Ascii85-style encoders handle a trailing partial block by emitting fewer than five digits and remembering how many characters came out. The output length is therefore not a multiple of five, and the decoder infers the original byte count from the partial digit count.

Option three: reject it. For a fixed key field, requiring a multiple of four bytes and refusing anything else is the honest choice. It surfaces the bug at the point where the bug is cheap to fix.

The Base85 codec exposes all three by pairing its strict block-length checkbox with its line-batch mode. In strict mode a length that is not a multiple of five is refused outright. In stream mode the trailing partial block is encoded and the result is annotated. Reading that annotation instead of guessing is the difference between a five-minute fix and an afternoon.

When you need to look at the raw bytes yourself rather than trust the annotation, a hex editor shows the same payload as offsets and bytes, and makes it obvious whether the tail is zeros, garbage, or a real value.

Batch mode is where encoding bugs become visible

Encoding a single 4-byte block is nearly impossible to get wrong by hand and nearly impossible to check by eye. Encoding a stream is neither. Once you have thousands of blocks, the interesting failures are not wrong characters, they are wrong structure: a truncated final block, a stray newline carried through from a copy-paste, a whitespace-only line that decodes to nothing, and a mixed-alphabet stream where one line was produced by a different library than the rest.

Line-batch mode exists precisely to expose that last failure. Feed a stream of one-block-per-line payloads through it and each line is validated independently against the selected alphabet. A line containing a character outside that alphabet is caught at that line, with the offending row identified, instead of the whole blob failing somewhere downstream.

This is the same reason a URL encoder is worth using even when the escaping looks trivial. Both tools convert an implicit assumption into an explicit, checked step. Batch validation turns “the payload decoded and I have no idea if it is right” into a per-row verdict.

Validating a custom alphabet before you trust it

Z85 and RFC 1924 are fixed. Some systems define their own 85-character alphabets, and that is where encoding bugs stop being subtle and start being loud.

A valid custom alphabet must satisfy three constraints, and the codec checks all three before it encodes anything:

  • Exactly 85 characters. Not 84, not 86. The alphabet length is the base of the numeral system, so a wrong length silently changes what every digit means.
  • All characters unique. A repeated character means two digit values map to the same glyph. Decoding then becomes ambiguous and some byte strings become unreachable.
  • All printable ASCII. No control characters, no non-ASCII bytes. A non-printable glyph in the alphabet produces output that breaks terminals, log pipelines, and copy-paste.

The uniqueness constraint is the one that catches real bugs. A 36-character alphabet accidentally pasted with a few duplicates looks plausible to the eye, decodes some inputs correctly by luck, and fails only on the specific byte patterns that happen to use the collided glyph. That failure surfaces at the receiver, hours later, on a different machine.

The printable-ASCII constraint is what makes a token copy-pasteable through email, chat, and shell history without a second escaping layer. Breaking it turns every transport system between you and the receiver into a potential corruption point.

Pick the alphabet once, and record which one you picked

The most valuable thing to carry alongside a Base85 payload is not the payload itself. It is the name of the alphabet. An unlabelled Base85 string is ambiguous data, because two completely different decoders will both accept it and return different bytes.

A durable record needs three fields. The variant name, so the receiver knows which of the 85 characters occupies each digit. The byte count before encoding, so the receiver can verify they got everything. The original length of the payload, so a trailing partial block can be reconciled rather than guessed at.

Keeping those three together with the encoded text turns an opaque token into a self-describing one. When the decoder rejects the payload six months later, the variant field answers the first question immediately, and the byte count answers the second.

Working with the alphabet itself is easier to reason about when you treat it as an explicit object rather than a library constant. The codec keeps the Z85 alphabet and the RFC 1924 alphabet as two named variants in the same interface, so switching between them is a selector change rather than a code change. Explore more tools at elysiatools.com.

What to check before you ship a Base85 payload

Before a Base85 field crosses a service boundary, walk this list in order. Confirm the variant, because a Z85 payload decoded as RFC 1924 is the single most common Base85 bug and it produces no error, just wrong bytes. Confirm the block length, since a strict encoder refuses a trailing partial block and a stream encoder must annotate one. Confirm the alphabet, by checking that every character in the output is a member of the declared 85-character set. Confirm the byte count, by comparing the decoded length against what the sender intended to send. Confirm the round trip, by decoding and re-encoding the value and checking that the original string comes back exactly.

The Base85 block contract and the three constraints every custom alphabet must satisfy

That last check is the cheapest and it catches everything else as a side effect. A round trip that returns the input byte-for-byte proves the variant, the alphabet, and the block length were all mutually consistent. A round trip that does not tells you which of the three to look at first.

The habit worth forming is the one this guide has followed throughout. Treat the alphabet as a named, validated, checked object instead of a constant buried in a library. The arithmetic is genuinely simple, four bytes in and five characters out, and that simplicity is exactly why the failure is silent when you get the alphabet wrong. Run the round trip, read the variant, and the whole family of Base85 bugs stops being a class of problem you have to worry about.

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply

    Your email address will not be published. Required fields are marked *