Base64 encoding across 5 languages: JavaScript, Python, Go, Rust, PHP
Idiomatic Base64 and Base64URL encoding/decoding in JS, Python, Go, Rust, and PHP — with the standard-library one-liners and the gotchas each language buries.
# The short version
Every major language has stdlib Base64 support. The API names differ; the output bytes are identical. Where they differ is how they handle:
- Strings vs bytes (does the decoder give you a byte array or a string?)
- Padding (some require
=, some don't) - The URL-safe variant (
-/_instead of+//) - Line breaks (some wrap output at 76 chars per the MIME spec)
<div class="callout callout-note" role="note"><div class="callout-title">Note</div><div class="callout-body"><p>If you're just looking to decode/encode one string in your browser, our <a href="/base64">Base64 tool</a> is faster than opening a REPL. This post is for when you're writing code.</p></div></div>
# JavaScript / TypeScript
Browser and Node both support btoa / atob, but they work on Latin-1 bytes — garbage for Unicode. Use the Buffer (Node) or TextEncoder (browser) for UTF-8-safe encoding.
// Node.js — the cleanest path
Buffer.from("Hello 🌍", "utf8").toString("base64");
// 'SGVsbG8g8J+MjQ=='
Buffer.from("SGVsbG8g8J+MjQ==", "base64").toString("utf8");
// 'Hello 🌍'
// Base64URL
Buffer.from("Hello", "utf8").toString("base64url");
// 'SGVsbG8' (no padding)
In the browser (no Node):
// UTF-8-safe encode
const b64 = btoa(String.fromCharCode(...new TextEncoder().encode("Hello 🌍")));
// Decode
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const text = new TextDecoder().decode(bytes);
Or use the modern standard (Safari 16+, Chrome 117+):
Uint8Array.fromBase64("SGVsbG8="); // native, 2024
# Python
Stdlib only — no dependencies needed.
import base64
base64.b64encode("Hello 🌍".encode("utf-8")).decode("ascii")
# 'SGVsbG8g8J+MjQ=='
base64.b64decode("SGVsbG8g8J+MjQ==").decode("utf-8")
# 'Hello 🌍'
# Base64URL
base64.urlsafe_b64encode(b"Hello").rstrip(b"=").decode("ascii")
# 'SGVsbG8'
<div class="callout callout-warning" role="note"><div class="callout-title">Warning</div><div class="callout-body"><p>Python's <code>base64.urlsafe_b64decode</code> is strict about padding. If your input omits <code>=</code> (as JWT does), pad it yourself: <code>s + "=" * (-len(s) % 4)</code>.</p></div></div>
# Go
Stdlib via encoding/base64. Choose the encoding variant explicitly — Go doesn't default.
import "encoding/base64"
encoded := base64.StdEncoding.EncodeToString([]byte("Hello 🌍"))
// "SGVsbG8g8J+MjQ=="
decoded, err := base64.StdEncoding.DecodeString(encoded)
// []byte("Hello 🌍")
// Base64URL (no padding — what JWTs use)
b64url := base64.RawURLEncoding.EncodeToString([]byte("Hello"))
// "SGVsbG8"
Go gives you four variants out of the box:
StdEncoding— standard, paddedRawStdEncoding— standard, no paddingURLEncoding— URL-safe, paddedRawURLEncoding— URL-safe, no padding (JWT flavour)
# Rust
The base64 crate is the de facto standard. Stdlib doesn't include it.
use base64::{Engine, engine::general_purpose::STANDARD};
let encoded = STANDARD.encode(b"Hello \xf0\x9f\x8c\x8d");
// "SGVsbG8g8J+MjQ=="
let decoded = STANDARD.decode(&encoded).unwrap();
// b"Hello \xf0\x9f\x8c\x8d"
// Base64URL, no padding (JWT)
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
URL_SAFE_NO_PAD.encode(b"Hello"); // "SGVsbG8"
If you're encoding large payloads (>10 MB), base64-simd is a drop-in replacement with 5–10× better throughput.
# PHP
Built in since PHP 4. No library needed.
base64_encode("Hello 🌍");
// "SGVsbG8g8J+MjQ=="
base64_decode("SGVsbG8g8J+MjQ==");
// "Hello 🌍"
// Base64URL — PHP has no built-in function; one-liner:
function base64url_encode(string $s): string {
return rtrim(strtr(base64_encode($s), "+/", "-_"), "=");
}
function base64url_decode(string $s): string {
return base64_decode(strtr($s, "-_", "+/") . str_repeat("=", (4 - strlen($s) % 4) % 4));
}
# Quick comparison
| Language | Stdlib? | Default variant | URL-safe built-in | Handles Unicode directly |
|------------|---------|-----------------|-------------------|--------------------------|
| JavaScript | Yes (Node) | Standard | Yes (base64url) | Via Buffer / TextEncoder |
| Python | Yes | Standard padded | Yes (urlsafe_*) | After .encode("utf-8") |
| Go | Yes | Explicit choice | Yes (4 variants) | Natively ([]byte) |
| Rust | Crate | Explicit engine | Yes (URL_SAFE*) | Natively (Vec<u8>) |
| PHP | Yes | Standard padded | Manual (one-liner)| Treats strings as bytes |
# The gotchas worth remembering
1. JavaScript's btoa is Latin-1-only. Pass it a string with any codepoint above 0xFF and it throws. Always go through Buffer or TextEncoder first.
2. Python's urlsafe_b64decode wants padding. Add = yourself for JWT-style unpadded input.
3. Go has four variants — if you use the wrong one, decode succeeds but produces wrong bytes. Pick once per codebase and stick with it.
4. Rust base64 crate changed API in 0.21. Old tutorials use base64::encode; the modern form is STANDARD.encode. Check your version.
5. PHP's base64_decode accepts garbage silently (returns false on strict mode, otherwise decodes what it can). Pass true as the second argument for strict validation.
# Try it
Paste any text into our Base64 tool to see the encoded result — or paste Base64 to decode. Everything runs in your browser; nothing uploaded.
# Related reading
Frequently asked questions
›Do all these languages handle Unicode the same way?
No — and this is the #1 bug source. Every language decodes Base64 back to bytes, but how those bytes become a string varies. JavaScript's `atob` gives you a binary string (latin-1); Python's `base64.b64decode` gives you bytes. Always explicitly decode as UTF-8 after.
›Is there a speed difference between stdlib and third-party Base64?
For a single encode/decode: no meaningful difference. For megabyte-scale throughput: yes — SIMD-accelerated libraries like `base64-simd` (Rust), `simdutf` (C++/JS via Node native) can be 5–10× faster than stdlib. Only worth reaching for if you're encoding large payloads at scale.
›Should I use Base64 or Base64URL?
Base64URL if the result will end up in a URL, a filename, or a JWT. Standard Base64 everywhere else. The difference is three characters: `+` → `-`, `/` → `_`, padding `=` is optional.