The Elixir Binary Model
Elixir has a huge advantage over Python and JavaScript when it comes to Base64: strings and binaries are the same thing. An Elixir string is a UTF-8-encoded binary — a sequence of bytes. The Base module operates on binaries, which means you can pass a string directly without any .encode("utf-8") or TextEncoder dance.
There is no separate "bytes type" to worry about, no coercion errors, and the output of Base.encode64 is itself a binary (a string containing only ASCII characters) that can be used anywhere a string is accepted.
Encoding a String — The Standard Pattern
# Strings are UTF-8 binaries — pass directly
encoded = Base.encode64("Hello 世界 👋")
# => "SGVsbG8g5LiW55WMIPCfkYs="
# Decode back
{:ok, decoded} = Base.decode64(encoded)
IO.puts(decoded) # "Hello 世界 👋"
# Or raise on error
decoded = Base.decode64!(encoded)
# Standard pattern inside a pipe chain
"Hello"
|> Base.encode64()
|> IO.puts()
# "SGVsbG8="URL-Safe Base64 for JWTs and Query Strings
# Standard Base64 — uses + and /
Base.encode64(<<251, 255>>)
# => "+/8="
# URL-safe Base64 — uses - and _
Base.url_encode64(<<251, 255>>)
# => "-_8="
# JWT style — URL-safe + no padding
payload = ~s({"user_id": 42, "role": "admin"})
Base.url_encode64(payload, padding: false)
# => "eyJ1c2VyX2lkIjogNDIsICJyb2xlIjogImFkbWluIn0"
# Decode without padding (JWT verification)
Base.url_decode64("eyJ1c2VyX2lkIjogNDJ9", padding: false)
# => {:ok, ~s({"user_id": 42})}Encoding a File
# Small to medium files — one-shot read
encoded =
"./image.png"
|> File.read!()
|> Base.encode64()
# Build a data URL for HTML embedding
mime = "image/png"
data_url = "data:#{mime};base64,#{encoded}"
# Large files — stream in chunks to avoid loading everything into memory
"./huge.bin"
|> File.stream!([], 3072) # 3072 bytes per chunk — divisible by 3 so no padding mid-stream
|> Enum.map(&Base.encode64/1)
|> Enum.join()
|> then(fn b64 -> File.write!("./huge.b64", b64) end)
# Why 3072 bytes? Base64 encodes 3 input bytes to 4 output characters. If a chunk size
# is a multiple of 3, each chunk produces padding-free Base64 that concatenates cleanly.Phoenix Framework Integration
# In a Phoenix controller — serve an inline image
def image(conn, %{"id" => id}) do
image_binary = MyApp.Images.fetch_binary!(id)
conn
|> put_resp_content_type("text/html")
|> send_resp(200, """
<img src="data:image/jpeg;base64,#{Base.encode64(image_binary)}" />
""")
end
# Signed state in a Phoenix.Token (uses Base.url_encode64 under the hood)
token = Phoenix.Token.sign(conn, "user auth", user_id)
# You rarely need Base.encode64 directly in Phoenix — the framework
# uses URL-safe Base64 for all its signed/encrypted token helpers.
# Phoenix.LiveView uploads — raw binary comes in as a binary already,
# Base.encode64 can be applied in the handle_event callback.
def handle_event("upload", _params, socket) do
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
data = File.read!(path) |> Base.encode64()
{:ok, data}
end)
endHTTP Basic Auth Header
defmodule MyApp.HTTPClient do
def basic_auth_header(user, password) do
creds = "#{user}:#{password}"
token = Base.encode64(creds)
{"authorization", "Basic " <> token}
end
end
# Use with HTTPoison / Req / Finch
headers = [MyApp.HTTPClient.basic_auth_header("admin", "secret")]
Req.get!("https://api.example.com/me", headers: headers)Erlang Interop
Elixir's Base module is a thin wrapper around Erlang's :base64module. You can call Erlang directly if you prefer, which is useful when porting Erlang code or in performance-sensitive loops where the extra function call matters:
# Erlang module interop
:base64.encode("Hello") # => "SGVsbG8="
:base64.decode("SGVsbG8=") # => "Hello"
# Benchmark reveals the two are essentially identical — call the Elixir
# version for readability unless you have a specific reason not to.
Benchee.run(%{
"Base.encode64/1" => fn -> Base.encode64("Hello World") end,
":base64.encode/1" => fn -> :base64.encode("Hello World") end
})Common Mistakes
- Looking for a Base64 module. It's called
Base, notBase64. The function isencode64, notencode. - Over-converting strings. Elixir strings are already binaries — no
.encode()step like Python. Passing:binary.list_to_bin("Hello")is unnecessary (and wrong — "Hello" is already a binary, not a list). - Using
decode64!on untrusted input. The bang variant raises on invalid input. For HTTP params or user data, useBase.decode64/1and pattern match on the{:ok, _}/:errortuple. - Padding mismatch. JWT uses URL-safe Base64 WITHOUT padding. Elixir keeps padding by default. Pass
padding: falseto both encode and decode when working with JWTs:Base.url_decode64(jwt_segment, padding: false). - Streaming with the wrong chunk size.If you concatenate Base64 chunks, each chunk must encode a multiple of 3 bytes or you'll have mid-stream padding that corrupts the output. Use chunks of 3072, 6144, etc.
Command Line Alternative
# Elixir one-liner from the shell
elixir -e 'IO.puts Base.encode64("Hello")'
# "SGVsbG8="
# From IEx
iex> Base.encode64("Hello")
"SGVsbG8="
# Or the built-in base64 command
echo -n "Hello" | base64Key Facts
- Module:
- Base (standard library — no dependency)
- Standard function:
- Base.encode64/1, Base.decode64/1, Base.decode64!/1
- URL-safe function:
- Base.url_encode64/1, Base.url_decode64/1
- Input type:
- binary (strings are binaries — no conversion)
- Padding option:
- padding: true (default) or padding: false
- Erlang equivalent:
- :base64.encode/1, :base64.decode/1
Related Base64 Tools
- Base64 Encode Online — general-purpose browser encoder
- Base64 Encode String — cross-language reference
- URL-Safe Base64 — JWT and query string variant
- Base64 Encode in Ruby — closely related functional language approach
- Base64 Decode Online — reverse the encoding
- JWT Debugger — inspect Phoenix.Token output