8 min read1,631 words

Base64 Encode and Decode Without Uploading

Illustration of browser developer tools and converters

Base64 turns arbitrary binary data into a string made up of letters, digits and a couple of symbols, which is why it shows up everywhere from email attachments to embedded images and API payloads. This post covers how the encoding works, why the built-in browser functions struggle with Unicode text, and where base64 helps or hurts in practice. The encoder and decoder under /dev-tools do all of this locally in the browser.

base64encodingjavascript
Share on XHacker News

What base64 is and the 33 percent increase

Base64 encodes binary data using an alphabet of 64 printable characters: uppercase and lowercase letters, digits, and two symbols, typically + and /. It works by grouping input bytes into chunks of three, which is 24 bits, and re-splitting those 24 bits into four 6-bit groups, each mapped to one character in the alphabet. Padding with = characters handles input lengths that are not a multiple of three.

Because three bytes of input always become four characters of output, and each output character represents only 6 usable bits of information rather than a full byte, base64 output is always about 33 percent larger than the original binary data. This overhead is the trade-off for being able to represent arbitrary bytes safely in contexts, like text fields or URLs, that only reliably support a limited character set.

Base64url

Standard base64 uses + and / in its alphabet, both of which have special meaning inside a URL, and it pads with =, which also needs escaping in some contexts. Base64url is a variant that replaces + with -, replaces / with _, and typically drops the trailing padding entirely, producing a string that is safe to place directly inside a URL path, query string or filename without further encoding.

JWTs, mentioned elsewhere on this site, use base64url for exactly this reason. Converting between the two alphabets is a simple character substitution plus optional padding adjustment, but a decoder built for standard base64 will not work on base64url input without that translation step first.

btoa, atob and the Unicode problem

JavaScript in the browser provides btoa to encode a string to base64 and atob to decode it back. Both functions operate on a string of single-byte characters, which was written into the specification when they were designed with binary-safe strings in mind rather than general Unicode text. Calling btoa directly on a string containing characters outside the Latin-1 range, such as emoji or many non-Latin scripts, throws an exception because those characters do not fit in a single byte.

The common workaround wraps the string through encodeURIComponent first to convert it into a form made only of characters btoa can handle, then unescapes the percent-encoded bytes back into raw characters before encoding. It works but is a genuine hack that has persisted mainly because it predates a cleaner alternative.

The modern Uint8Array approach

The cleaner fix is to work with actual bytes instead of the string workaround. TextEncoder converts a JavaScript string into a Uint8Array of UTF-8 bytes, which can then be base64 encoded byte by byte without any Latin-1 assumption, and TextDecoder reverses the process after decoding. Newer JavaScript engines are also adding base64 methods directly on Uint8Array and Uint8Array.fromBase64, which removes the need for the btoa and atob detour entirely once that support is available in your target environments.

For any code handling text that might contain non-ASCII characters, which is most real-world text, going through TextEncoder and TextDecoder rather than calling btoa and atob directly on a raw string avoids the Unicode exception altogether.

Data URIs and caching

A data URI embeds base64-encoded content directly inside an HTML or CSS file using the data: scheme, for example data:image/png;base64,iVBORw0KGgo.... This avoids a separate HTTP request for small assets like icons, which can help when a page makes many small requests and the overhead of each request outweighs the benefit of caching it separately.

The trade-off is that a data URI cannot be cached independently of the document that contains it, since it is not a separate resource with its own URL. An icon embedded as a data URI in a CSS file gets re-downloaded every time that CSS file changes, even if the icon itself never changed, and the 33 percent size overhead of base64 makes the embedded version larger than the original file would have been on its own. For anything reused across many pages or large enough to benefit from long-term caching, a normal linked file is usually the better choice.

Base64 is not encryption

Base64 is an encoding, not a cipher. It has no key, no secret, and no security property whatsoever; anyone who sees a base64 string can decode it back to the original data using nothing more than a standard library function. Systems that store passwords or tokens as "encoded" base64 rather than properly hashed or encrypted values are not protecting that data at all.

It is worth being explicit about this distinction with anyone who might assume otherwise, since a base64 string does look unreadable at a glance, which is exactly the visual cue that makes people mistakenly assume it offers some protection.

Doing it locally

Encoding and decoding base64 requires no network access at all, since every step described here runs on data already in memory in the browser. The base64 tool under /dev-tools handles standard and base64url variants, works correctly with Unicode text through the TextEncoder approach, and never sends the input anywhere, which matters if the content you are encoding or decoding is not something you want leaving your machine.

Worked example: encoding and decoding a short string

Encoding "Hi" gives SGk=. The two input bytes, H (01001000) and i (01101001), form 16 bits, which base64 groups into 6-bit chunks: 010010, 000110, 1001, and the last group is padded with two zero bits to make a full 6 bits, giving 100100. Each 6-bit value maps to a character: 18 is S, 6 is G, 36 is k, and the final group, having been padded with the actual data, still contributes a valid character, with a single = added because the original input length was not a multiple of three bytes.

Decoding reverses this exactly: SGk= maps back through the alphabet to the same 6-bit groups, which recombine into the original two bytes, H and i, discarding the bits that were only there as padding to reach a full byte boundary.

Edge cases in base64 handling

Line length limits are a legacy of email encoding: the original MIME specification for base64 requires inserting a line break every 76 characters, and some older libraries still do this by default, which produces output that looks broken to a decoder expecting a single unbroken string until the newlines are stripped first. Whitespace inside a base64 string, including line breaks, is technically outside the alphabet and most decoders simply ignore it, but not all of them do, so mixing an email-style base64 blob into a context expecting a single-line string can fail unexpectedly.

Missing or incorrect padding is the other frequent issue: a base64 string should have a length that is a multiple of four after padding is included, and a string that is short by one or two characters needs the equivalent number of = characters appended before some strict decoders will accept it, even though base64url intentionally omits this padding by convention.

  • MIME-style base64 inserts a line break every 76 characters; strip these before decoding elsewhere
  • Some decoders reject internal whitespace rather than ignoring it
  • A base64 string length should be a multiple of four once padding is included
  • base64url intentionally omits padding, which strict base64 decoders may not accept without adjustment

Debugging checklist

If atob throws an exception, the input almost always either contains a character outside the base64 alphabet, has been double-encoded (encoded twice by mistake), or is actually base64url rather than standard base64 and needs its - and _ characters translated back to + and / first. If btoa throws, the input string very likely contains a character outside the Latin-1 range and needs to go through TextEncoder rather than being passed to btoa directly.

If a decoded value looks like valid text but is subtly wrong, for example a currency symbol turning into multiple strange characters, that is the classic symptom of decoding UTF-8 bytes as if they were Latin-1 one byte at a time, and the fix is to route the decoded bytes through TextDecoder configured for UTF-8 rather than building a string character by character from raw byte values.

  • atob failures usually mean an invalid character, double encoding, or unconverted base64url input
  • btoa failures usually mean the input string has non-Latin-1 characters and needs TextEncoder instead
  • Garbled multi-byte characters after decoding point to a Latin-1 versus UTF-8 mismatch
  • Confirm whether the source system expects standard or url-safe base64 before comparing outputs

FAQ

Is base64 reversible without any key? Yes, always. There is nothing secret about base64; anyone with the encoded string and a standard library can decode it back to the original bytes, which is why it must never be used as a substitute for actual encryption or hashing.

Why do some base64 strings end in one or two equals signs and others end in none? Padding depends on the length of the original input modulo three. An input length divisible by three needs no padding, a remainder of one byte needs two padding characters, and a remainder of two bytes needs one, and base64url conventionally drops this padding entirely since URL contexts do not require a fixed-length encoding.

Can I base64 encode a large file directly in the browser? Yes, for reasonably sized files, though very large files can be slow or memory-heavy to encode as a single string in JavaScript, and a streaming approach that processes chunks is worth considering once files reach into the hundreds of megabytes.

Questions about the tools in this guide

Short answers about the hubs this article touches, each linking straight to the tool.

Dev Tools

Open hub