Bases e bytes
base16, base62, base64, url-safe, UTF-8, 1 byte, 4 bytes. São conceitos que aparecem em encurtadores de URL, geração de identificadores, cabeçalhos HTTP e no dimensionamento de colunas do banco de dados — e que costumam ser usados sem que a diferença entre caractere e byte esteja clara.
O que significa baseX
A regra é direta: em uma baseX, apenas X caracteres distintos são permitidos para representar um valor. O conjunto desses caracteres é chamado de alfabeto da base.
| Base | Caracteres | Alfabeto |
|---|---|---|
base2 | 2 | 0, 1 |
base8 | 8 | 0 – 7 |
base16 | 16 | 0 – 9, A – F |
base62 | 62 | 0 – 9, a – z, A – Z |
base64 | 64 | 0 – 9, a – z, A – Z, +, / |
base64 url-safe | 64 | 0 – 9, a – z, A – Z, -, _ |
Quanto maior a base, menos caracteres são necessários para representar o mesmo valor:
decimal 255 → base2: 11111111 (8 caracteres)
→ base8: 377 (3 caracteres)
→ base16: FF (2 caracteres)
base16: o formato de depuração
Cada byte vira exatamente dois caracteres hexadecimais, o que torna a representação previsível e fácil de inspecionar:
Buffer.from('usuario').toString('hex');
// '7573756172696f' → 7 bytes viram 14 caracteres
É o formato de hashes (sha256), UUIDs e dumps binários. O custo é dobrar o tamanho da representação.
base64: transportar binário como texto
O base64 agrupa os bits de 3 bytes (24 bits) e os reparte em 4 blocos de 6 bits. Como 2⁶ = 64, cada bloco vira um caractere do alfabeto:
texto: S o l
bytes: 01010011 01101111 01101100
grupos: 010100 110110 111101 101100
base64: U 2 9 s
Buffer.from('usuario').toString('base64');
// 'dXN1YXJpbw==' → 7 bytes viram 12 caracteres
Duas consequências práticas:
- O tamanho cresce cerca de 33%: a cada 3 bytes de entrada, saem 4 caracteres;
- O
=é preenchimento (padding): entra quando a entrada não é múltiplo de 3 bytes, apenas para completar o último bloco de 4.
base64 não é segurançabase64 é codificação, não criptografia nem compressão. Qualquer pessoa decodifica o valor com um comando. É por isso que o payload de um JWT, mesmo parecendo ilegível, pode ser lido por qualquer um — o que o protege é a assinatura, não a codificação.
base64 url-safe: por que existe
No base64 tradicional, dois caracteres do alfabeto têm significado especial em uma URL:
+é interpretado como espaço em query strings;/é o separador de caminho.
A variante url-safe (RFC 4648, seção 5) resolve o problema trocando + por - e / por _, e normalmente omitindo o padding =:
Buffer.from('usuario').toString('base64url');
// 'dXN1YXJpbw' → sem '=' e sem caracteres problemáticos
É a codificação usada em JWT, tokens de redefinição de senha e qualquer valor que trafegue em URL ou nome de arquivo.
base62: identificadores curtos e legíveis
O base62 é o base64 sem +, / e sem padding — ou seja, apenas letras e números. Ele não precisa de escape em URL, não quebra ao ser copiado de um e-mail e não gera ambiguidade em um double-click.
Por isso é a escolha clássica de encurtadores de URL:
7 caracteres em base62 = 62⁷ = 3.521.614.606.208 combinações
Mais de três trilhões de identificadores em apenas 7 caracteres — motivo pelo qual https://bit.ly/aiksAi7 cabe em 22 bytes.
- Precisa inspecionar ou comparar visualmente?
base16. - Precisa transportar binário em um campo de texto?
base64. - O valor vai para uma URL, cabeçalho ou nome de arquivo?
base64urloubase62.
Bytes: nem todo caractere ocupa o mesmo espaço
Aqui está a confusão mais comum: caractere não é byte. Em UTF-8 — a codificação padrão da web e da maioria dos bancos de dados modernos — o espaço ocupado é variável.
| Bytes | O que cabe | Exemplos |
|---|---|---|
| 1 | Caracteres ASCII: 0 – 9, a – z, A – Z e pontuação básica (/, -, _, :, .) | a, 7, - |
| 2 | Letras acentuadas, cedilha, alfabetos grego, cirílico, árabe e hebraico | ç, á, ñ, ж |
| 3 | Ideogramas CJK (chinês, japonês, coreano) e a maioria dos símbolos | 日, 本, € |
| 4 | Emojis e caracteres suplementares | 😀, 𝄞 |
É frequente ouvir que japonês e chinês ocupam 2 bytes — isso vale para UTF-16, não para UTF-8. Em UTF-8, ideogramas CJK ocupam 3 bytes. Quem ocupa 2 bytes são os acentuados latinos e alfabetos como o cirílico.
Contando na prática
7 bytes: u s u a r i o
| | | | | | |
1 1 1 1 1 1 1
9 bytes: e n d e r e ç o
| | | | | | | |
1 1 1 1 1 1 2 1
O ç sozinho explica a diferença entre os dois valores abaixo:
| Valor | Caracteres | Bytes |
|---|---|---|
usuario | 7 | 7 |
catalogo de produtos web | 24 | 24 |
catálogo de produtos web | 24 | 25 |
https://bit.ly/aiksAi7 | 22 | 22 |
https://www.lojaexemplo.com.br/departamento-de-vendas/produtos/... | 100 | 100 |
Em JavaScript, o length da string não responde essa pergunta:
const campo = 'endereço';
campo.length; // 8 → unidades de código UTF-16
Buffer.byteLength(campo, 'utf8'); // 9 → bytes reais em UTF-8
const emoji = '😀';
emoji.length; // 2 → um par substituto (surrogate pair)
Buffer.byteLength(emoji, 'utf8'); // 4 → bytes reais em UTF-8
[...emoji].length; // 1 → caracteres percebidos
String.prototype.length conta unidades UTF-16, o spread conta pontos de código e Buffer.byteLength conta bytes. Validar um limite de tamanho com length deixa passar valores que estouram o limite real de armazenamento ou de transmissão.
Por que isso importa
Dimensionamento de colunas no banco
VARCHAR(255) significa coisas diferentes conforme o banco e o charset:
- PostgreSQL:
VARCHAR(255)são 255 caracteres, independentemente de quantos bytes cada um ocupa; - MySQL com
utf8mb4:VARCHAR(255)também são 255 caracteres, mas o armazenamento pode chegar a 1.020 bytes — e é o limite em bytes que conta para o índice.
No MySQL/InnoDB, o limite de uma chave de índice é de 3.072 bytes. Com utf8mb4 (4 bytes por caractere no pior caso), uma coluna VARCHAR(768) já ocupa todo o espaço disponível. Criar um índice em uma coluna maior falha — ou é silenciosamente truncado, dependendo da configuração.
Limites de protocolo são medidos em bytes
Praticamente todo limite de infraestrutura é expresso em bytes, nunca em caracteres:
| Limite | Valor típico |
|---|---|
| Cookie (por cookie) | 4.096 bytes |
| Cabeçalhos HTTP (nginx, por padrão) | 8 KB |
| URL (limite prático em navegadores) | ~2.000 caracteres |
| Documento no MongoDB | 16 MB |
| Mensagem no Kafka (padrão) | 1 MB |
Um JWT com claims em português acentuado ocupa mais espaço do que a contagem de caracteres sugere — e o base64url ainda acrescenta os ~33% de overhead sobre o total em bytes.
Truncar texto é mais delicado do que parece
Cortar uma string por bytes pode partir um caractere ao meio, produzindo o caractere de substituição �:
const texto = 'preço';
// errado: corta no meio do caractere multibyte
Buffer.from(texto).subarray(0, 4).toString('utf8'); // 'pre�'
// correto: opera sobre caracteres, não bytes
[...texto].slice(0, 4).join(''); // 'preç'
O mesmo cuidado vale para emojis, cujo corte incorreto quebra o par substituto e gera um caractere inválido.
Conclusão
Base e byte respondem perguntas diferentes: a base define quais caracteres podem representar um valor e, portanto, quantos caracteres serão necessários; o byte define quanto espaço cada caractere realmente ocupa.
Na prática, três pontos evitam a maior parte dos problemas:
- Escolha a base pelo destino do valor —
base16para inspeção,base64para transporte,base64urlebase62para URLs; - Lembre que
base64cresce cerca de 33% e não oferece nenhuma proteção — é codificação, não criptografia; - Meça limites em bytes, não em caracteres: um único
çjá quebra a conta, e um emoji vale por quatro.
