& ©'
decodeHtml('<>'); // → '<>'
decodeHtml('😀'); // → '😀'
decodeHtml('© 2026'); // → '© 2026' (lenient, no semicolon)
// ---------------------------------------------------------------
// SECURITY: decoded text is unescaped. Never do this with untrusted input:
// el.innerHTML = decodeHtml(userInput); // ❌ reopens the XSS hole
// If the decoded value must be displayed, re-escape it in its destination context first,
// or assign it as text:
// el.textContent = decodeHtml(userInput); // ✅ shown as literal text
// ---------------------------------------------------------------
// Node.js (no DOM) — use a tested library such as he:
// import { decode } from 'he';
// decode('<div> & ©'); // → '
& ©'
```
#### FAQ
**Q: Is my text sent to your server when I decode it?**
A: No. Every entity is resolved entirely in your browser with JavaScript — open DevTools → Network and you will see zero requests fire when you type or paste. Nothing is uploaded, nothing is logged, nothing is written to disk. That privacy matters because the escaped strings people decode are often sensitive: a fragment pulled from a private database, an internal email, a customer record, or markup copied out of an application you do not want leaking. On a server-side decoder every one of those would travel across the network to a machine you do not control; here the text never leaves the tab. This is the whole reason to decode HTML client-side rather than paste it into a website that could, in principle, keep a copy of everything it processes.
**Q: What does it mean to decode or unescape HTML?**
A: Decoding HTML — also called unescaping — is the reverse of HTML escaping: it takes character references like <, &, <, or © and converts each back into the real character it stands for (<, &, <, ©). You reach for it whenever you have a string that was stored or transmitted in its escaped form and you need the literal text back — to read it, edit it, feed it to another program, or debug why a page is showing < on screen instead of <. If you want to go the other way and turn characters into entities, use the companion
HTML Entity Encoder ; the two are exact inverses.
**Q: Which kinds of entities can this decoder handle?**
A: All three forms, in any mix. It resolves named references (<, &, ©, — and the full HTML5 named-entity set), decimal numeric references (<, é), and hexadecimal numeric references (<, é). It also reconstructs astral-plane characters above U+FFFF from their numeric references, so an emoji like 😀 decodes correctly to 😀. And it follows the browser's lenient parsing for a handful of legacy named entities that omit the trailing semicolon — © 2026 still decodes to © 2026 — which strict parsers would skip. In short, whatever an encoder produced, this decoder reverses it.
**Q: Why does my text show < instead of **
A: That is the classic symptom of double-encoding. Somewhere in your pipeline the text was escaped twice: the first pass turned < into <, and the second pass turned the & in < into &, giving <. When the browser decodes that once, it shows < as literal text rather than <. To recover the original, decode it twice — paste the string here to get <, then paste that result back in to get <. The real fix is upstream: escape exactly once, at output time, so the text is never double-encoded in the first place.
**Q: Will the decoded output be safe to put back into a page?**
A: Be careful here. Decoding is the opposite of escaping, so decoded text is by definition unescaped — if it contains a <script> tag or an onerror handler, that markup is now live again. Never take untrusted input, decode it, and insert the result into your page with innerHTML, or you reintroduce exactly the cross-site-scripting (XSS) hole that escaping was meant to close. Decoding is the right move when you need the raw characters for reading, editing, or storage; but anything you render back into HTML must be re-escaped in its destination context. If you are about to display the decoded result, run it through the
HTML Entity Encoder again first.
**Q: Does decoding handle non-ASCII characters and emoji correctly?**
A: Yes. Numeric references can encode any Unicode code point, and the decoder resolves them all — accented letters (é → é), symbols (€ → €), em dashes (— → —), and full-plane emoji (😀 → 😀). For astral characters above U+FFFF it reconstructs the complete code point rather than producing a broken half-character. Raw non-ASCII characters that are already in the input pass through untouched, so a string that mixes real UTF-8 with entities decodes cleanly without corrupting either part. Make sure the page or file you paste the result into is served as UTF-8 so the recovered characters display correctly.
**Q: How do I encode text back into entities?**
A: Use the companion
HTML Entity Encoder . It takes raw characters like <div> & © and escapes them to <div> & ©, with options for named, decimal, or hex output and an "encode all non-ASCII" mode for legacy charsets. Encoding and decoding are exact inverses for the reserved characters, so you can round-trip text through both tools without loss. You can jump straight there with the Swap direction button on this page.
**Q: Is this the same as URL decoding or Base64 decoding?**
A: No — they are three different encodings for three different jobs, and mixing them up is a common source of bugs. HTML entity decoding turns < back into <. URL (percent) decoding turns %20 back into a space and is for query strings and paths — use the
URL Encoder / Decoder for that. Base64 decoding turns a base64 string back into the original bytes and is for binary-safe transport — use
Base64 Encode / Decode . A value can be wrapped in more than one of these, so decode them in the reverse order they were applied. This tool handles HTML entities only.
---
### Free HTML Entity Encoder — Escape HTML
URL: https://go-tools.org/tools/html-entity-encode
Encode HTML entities and escape special characters (< > & " ') online — free, no signup, 100% in your browser. Named, decimal, or hex output; never uploaded.
#### What is HTML entity encoding?
HTML entity encoding — also called HTML escaping — is the process of replacing characters that have special meaning in HTML with a safe textual representation called an entity, so the browser displays them as literal text instead of interpreting them as markup. The five characters that matter most are the ones HTML uses to structure a document: the angle brackets < and > that open and close tags, the ampersand & that begins an entity, and the quotation marks " and ' that delimit attribute values. When any of these appears in content that should be shown rather than executed, it must be escaped, or the browser will misread the page — at best your text renders wrong, at worst an attacker slips in a <script> tag.
It helps to be precise about what this tool does. It encodes text into entities; it does not assemble or pretty-print a document. If you want to read a string of code on a page as plain text, or you are inserting user-supplied input into your HTML and need to neutralise it, this is the right tool. If instead you want to indent and tidy existing markup, that is the job of the
HTML Formatter ; and to turn entities back into characters, use the
HTML Entity Decoder .
There are three ways to write any entity, and they are interchangeable. A named reference uses a human-friendly label (< for <, © for ©); a decimal numeric reference writes the character's Unicode code point in base 10 (< for <); and a hexadecimal reference writes the same code point in base 16 (< for <), matching the U+XXXX notation of the Unicode standard. Named entities read best but exist only for characters that have a defined name; numeric entities can represent any code point, which is why they are the safe fallback. The table below lists the entities you will reach for most often:
| Character | Named | Decimal | Hex |
|-----------|-------|---------|-----|
| < | < | < | < |
| > | > | > | > |
| & | & | & | & |
| " | " | " | " |
| ' | ' | ' | ' |
| (space) | |   |   |
| © | © | © | © |
| ® | ® | ® | ® |
| ™ | ™ | ™ | ™ |
| € | € | € | € |
| £ | £ | £ | £ |
| — | — | — | — |
| – | – | – | – |
| … | … | … | … |
| é | é | é | é |
Note that the apostrophe is written ' (or ') rather than ': the named ' was only standardised in HTML5 and XML and is unsafe in older HTML4 parsers, so the numeric form — understood everywhere — is the compatible choice. This tool follows the same convention as the widely used he library, which is why the default output for ' is '.
The distinction between a character set and an entity is worth holding onto, because it explains the "Encode all non-ASCII" option. A charset (like UTF-8) determines how characters are stored as bytes; an entity is a way to write a character using only the plain ASCII characters & # ; and letters or digits. On a modern UTF-8 page, é, —, and 😀 are valid raw characters and need no entity at all — which is why the default mode leaves them alone. You only force them into entities when the text must pass through a system that cannot handle raw UTF-8, in which case every non-ASCII code point is rewritten as an ASCII-safe numeric or named reference. And because all of this runs in your browser, the markup you escape — even a private template or an unpublished draft — never crosses the network. For related conversions, the
JSON Escape and
Base64 Encode / Decode tools handle escaping for JavaScript strings and binary-safe transport respectively.
```
// Server-side templates auto-escape, but when you build HTML by hand you must escape yourself.
// The five reserved characters and their safe entities:
// < → < > → > & → & " → " ' → '
// Node.js — escape untrusted input before inserting it into HTML element content.
function escapeHtml(str) {
return str
.replace(/&/g, '&') // & first, so later entities are not double-escaped
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, '''); // numeric form — safe in HTML4, HTML5 and XML
}
const userInput = `
Tom & Jerry's `;
const safe = escapeHtml(userInput);
// → <a href="x">Tom & Jerry's</a>
document.getElementById('out').innerHTML = `
${safe}
`; // renders as literal text
// ---------------------------------------------------------------
// In practice, prefer the platform's built-in escaping where it exists:
// - React / Vue / Angular escape interpolated text by default
// - Use textContent instead of innerHTML when you only need text:
// el.textContent = userInput; // the browser escapes for you
// - Server frameworks (Jinja, ERB, Blade) auto-escape unless you opt out
```
#### FAQ
**Q: Is my text sent to your server when I encode it?**
A: No. Every character is encoded entirely in your browser with JavaScript — open DevTools → Network and you will see zero requests fire when you type or paste. Nothing is uploaded, nothing is logged, nothing is written to disk. That privacy matters because the markup people escape is often sensitive: a snippet from a private CMS, an internal email template, a customer support reply, or a draft blog post you have not published. On a server-side encoder every one of those would travel across the network to a machine you do not control; here the text never leaves the tab. This is the whole reason to escape HTML client-side rather than paste it into a website that could, in principle, keep a copy of everything it processes.
**Q: What does it mean to escape HTML, and why would I do it?**
A: Escaping HTML means replacing characters that the browser would otherwise interpret as markup with their entity equivalents, so they are displayed as literal text instead. The classic case is showing code on a page: if you want a visitor to read the string <strong>bold</strong> rather than see the word "bold" rendered in boldface, you escape the angle brackets to <strong>bold</strong>. The other, more important case is security: when you insert untrusted user input into a page, escaping the five reserved characters (< > & " ') prevents that input from breaking out of its context and injecting a <script> tag — the core defense against cross-site scripting (XSS). Any text that originates from a user and lands in your HTML should be escaped first.
**Q: What is the difference between named, decimal, and hex entities?**
A: All three produce the same character; they differ only in how the reference is written. A named entity uses a human-readable label — < for <, & for &, © for © — which is easy to read but only works for characters that have a defined name. A decimal numeric entity writes the Unicode code point in base 10, like < for < or é for é. A hexadecimal entity writes the same code point in base 16, like < for < or é for é, mirroring the U+XXXX notation in the Unicode standard. Named entities are the most readable and are the right default for the common reserved characters; numeric entities (decimal or hex) can encode any code point, including ones with no name, which makes them the safe choice when you cannot guarantee the consumer supports a particular named entity.
**Q: Why is the apostrophe encoded as ' and not '?**
A: Because ' is not safe everywhere. The named entity ' was only introduced in HTML5 and XML — it is not defined in HTML4, so a few older parsers and email clients render it as the literal text "'" instead of an apostrophe. The numeric reference ' (or its decimal twin ') maps to the exact same character, U+0027, and is understood by every conforming parser ever written. Following the behavior of well-tested libraries like he, this tool emits the universally compatible ' for the apostrophe so the output is safe to drop into any HTML, XML, or attribute context without surprises.
**Q: Do I need to encode non-ASCII characters like é, — or 😀?**
A: Usually no. If your page declares <meta charset="utf-8"> — which essentially every modern page does — then accented letters, em dashes, and emoji are perfectly valid as raw UTF-8 and need no encoding at all. That is why the default "special characters" mode leaves them untouched, keeping your output short and readable. You only need to encode non-ASCII characters when the text will be served or stored in a legacy single-byte charset, or passed through a system that corrupts raw UTF-8. For those cases tick "Encode all non-ASCII characters" and every code point above 0x7F is converted to an ASCII-safe entity. When in doubt, keep the default and make sure your charset declaration is correct.
**Q: Does escaping HTML protect me from XSS attacks?**
A: Escaping is the foundation of XSS defense, but it is context-dependent, so the honest answer is "yes, when applied correctly." Encoding the five reserved characters before you place untrusted input into HTML element content reliably stops an attacker from injecting tags or scripts — a payload like <script>alert(1)</script> becomes inert text. The caveat is that HTML has several contexts, each with its own escaping rules: inside an attribute value you must escape quotes (which this tool does), inside a <script> block or an inline event handler you need JavaScript escaping instead, and inside a URL you need URL encoding. Use HTML entity encoding for HTML and attribute contexts; for URLs reach for the
URL Encoder / Decoder , and for embedding a string in JavaScript or JSON see the
JSON Escape tool. Encode at output time, in the context where the data lands.
**Q: How do I reverse this — turn entities back into characters?**
A: Use the companion
HTML Entity Decoder . It takes a string full of entities like <div> & © and converts it back to the real characters <div> & ©, handling named entities, decimal references, hexadecimal references, and even legacy unterminated entities such as © without a trailing semicolon. Encoding and decoding are exact inverses for the reserved characters, so you can round-trip text through both tools without loss. If you are debugging why a page shows literal < instead of <, the decoder is the fastest way to see what the entities actually resolve to.
**Q: Will encoding change the visible text or break my layout?**
A: No — that is the entire point. An entity is just an alternate spelling of a character: when a browser parses < it renders a single < glyph, identical to the raw character. So a correctly escaped page looks exactly the same to a visitor as it would with raw characters; the only difference is that the browser treats the escaped version as text rather than markup. The one thing escaping changes is the length and appearance of the source string, which is why you escape only what needs escaping. If your goal is to clean up and indent messy markup rather than escape it, that is a different job — use the
HTML Formatter instead.
---
### HTML Formatter, Beautifier & Minifier
URL: https://go-tools.org/tools/html-formatter
Format, beautify and minify HTML instantly in your browser. Indent messy markup or compress it to ship — free, private, and your HTML never leaves your device.
#### What is HTML Formatting?
HTML formatting (also called beautifying or pretty-printing) rewrites markup with consistent nesting, indentation and line breaks so its structure is easy to read and edit. The page renders identically before and after — only whitespace changes. Minifying does the reverse: it removes comments and collapses whitespace — including embedded CSS and JS — so pages load faster. This tool does both, entirely in your browser.
#### FAQ
**Q: How do I format HTML online?**
A: Paste your HTML into the input box and click Format. The tool reindents the markup with proper nesting and line breaks, then lets you copy it. Everything runs locally in your browser — nothing is uploaded.
**Q: How do I minify HTML?**
A: Paste your HTML and click Minify. The tool removes comments and collapses whitespace — including embedded CSS and JavaScript — to produce the smallest equivalent markup, and shows how many bytes you saved.
**Q: What is the difference between formatting and minifying HTML?**
A: Formatting (beautifying) adds indentation and line breaks to make markup readable. Minifying strips comments and whitespace to shrink the file for faster loading. Both render identically in the browser.
**Q: Does formatting change how my page renders?**
A: Formatting only adds whitespace, which is safe for normal markup. Be aware that whitespace-sensitive elements like pre and textarea can be affected by reformatting or aggressive minification — verify those after processing.
**Q: Is my HTML safe with this tool?**
A: Yes. All formatting and minifying happen locally in your browser using JavaScript — your HTML is never sent to any server, logged, or stored. That makes it safe for proprietary or unreleased markup, unlike server-side tools that receive a copy of everything you paste.
**Q: Does it minify inline CSS and JavaScript?**
A: Yes. The minifier compresses style and script contents too, so a single pass shrinks your whole document — markup, styles and scripts together.
**Q: What indentation should I use for HTML?**
A: Two spaces is the most common default and keeps diffs compact; four spaces can help with deeply nested layouts; tabs let each developer choose their width. Pick one and apply it consistently — this tool supports all three.
---
### HTML to Markdown Converter
URL: https://go-tools.org/tools/html-to-markdown
Convert HTML to clean Markdown in your browser — GFM tables, task lists, and links. Choose ATX/Setext headings and inline or reference links. Great for migrating web content or feeding LLMs. 100% private, no upload.
#### What is HTML to Markdown Conversion?
HTML to Markdown conversion takes a rendered HTML document — the tags, attributes, and nesting a browser displays — and rewrites it as Markdown, the lightweight plain-text format built for writing and version control. Where Markdown to HTML expands compact text into markup for display, this is the reverse and reductive direction: you start with rich, verbose HTML and distil it down to the small, readable set of conventions Markdown offers.
Under the hood the converter parses your HTML into a DOM tree — the same node structure a browser builds — then walks that tree and emits the Markdown equivalent for each node it recognises. An <h2> becomes ## , a <strong> becomes **text**, a <ul> becomes a bulleted list, an
becomes a link, a <table> becomes a GFM pipe table. Traversing a real DOM, rather than running regular expressions over the raw string, is what lets it handle nested lists, mixed inline formatting, and tables correctly instead of breaking on edge cases.
You reach for this conversion when you are migrating out of HTML, not into it. Content trapped in a CMS, a WYSIWYG editor, an old web page, or a rich-text field is hard to diff, hard to review, and hard to move. Converting it to Markdown frees it into a format that lives happily in a Git repo, a static-site generator, or a notes app — and, increasingly, into a format that large language models read efficiently. The catch, which honest tools state plainly, is that the conversion is lossy: HTML can express things Markdown cannot, so some structure and every styling detail are deliberately discarded in exchange for clean, portable text.
The reverse operation — Markdown back to HTML, for when you are ready to publish or preview — is just as useful. Switch to the Markdown → HTML tab or open the dedicated Markdown to HTML converter .
```
HTML in:
Pricing
Plans start at $9/mo . See the details .
Markdown out:
## Pricing
Plans start at **$9/mo**. See the [details](https://example.com/pricing).
| Plan | Price |
| ---- | ----- |
| Pro | $9 |
```
#### FAQ
**Q: How are inline vs reference links handled?**
A: You choose with the Links radio. Inline style writes each anchor as [text](url) right where it appears — compact and obvious for one or two links per paragraph. Reference style writes [text][1] in the prose and collects all the URLs as [1]: https://… definitions at the bottom of the document, which keeps text with many links readable and lets you reuse a URL by label. Both produce identical rendered output; it is purely a source-readability choice. Images follow the same rule: an <img> becomes  inline or ![alt][1] in reference mode.
**Q: ATX vs Setext headings — which should I use?**
A: ATX headings prefix the line with hashes — # H1, ## H2, ### H3 — and work for all six levels. Setext headings underline the text instead: a row of = under a line makes it an H1, a row of - makes it an H2. The catch is that Setext only exists for levels 1 and 2, so this converter emits Setext for <h1>/<h2> and automatically falls back to ATX for <h3> and deeper. ATX is the more common, more portable choice and is easier to grep; pick Setext only if a downstream style guide or linter requires it.
**Q: What happens to HTML that Markdown can't represent, like <div> and <span>?**
A: Markdown has no syntax for generic containers, so structural wrappers such as <div>, <span>, <section>, and <article> are unwrapped — their text and child elements are kept, but the tag itself disappears because there is nothing in Markdown to map it to. Class names, id attributes, inline style attributes, and data-* attributes are dropped for the same reason: Markdown carries no way to express them. When an element genuinely has no Markdown equivalent and dropping it would lose meaning, the converter leaves it as raw inline HTML rather than silently deleting the content. This is by design — see the question on whether the conversion is lossless.
**Q: Does it strip <script> and styles?**
A: Yes. <script> and <style> elements, along with their contents, are removed entirely — they are code and CSS, not document content, and have no place in Markdown. The same goes for <link>, <meta>, and other head-level elements when you paste a whole page. Inline event handlers like onclick and CSS in style attributes are dropped as well. The result is text content only, which is exactly what you want when the Markdown is headed for a docs repo, a static-site generator, or an LLM context window. If you need the styling preserved, Markdown is the wrong target format.
**Q: How are nested tables and lists handled?**
A: Nested lists convert cleanly: each level of <ul>/<ol> nesting becomes two spaces of indentation, and ordered lists are renumbered from 1. Tables are trickier. GitHub Flavored Markdown pipe tables are flat by specification — a table cell cannot contain another table, and it cannot contain block elements like lists or multiple paragraphs. So a simple <table> converts to a clean pipe table, but a table with a nested table inside a cell, or with block content in cells, degrades: the converter flattens what it can and falls back to leaving the complex parts as raw HTML so no data is lost. Deeply nested layout tables from legacy pages are the worst case — consider simplifying the HTML first.
**Q: Is HTML to Markdown lossless?**
A: No, and it is important to be honest about that. HTML is far more expressive than Markdown: it has hundreds of elements and arbitrary attributes, while Markdown covers a small, deliberate set — headings, emphasis, lists, links, images, code, blockquotes, and (with GFM) tables, task lists, and strikethrough. Anything outside that set has no representation: colspans, custom attributes, inline styles, <div>/<span> structure, and most semantic wrappers are dropped or preserved only as raw HTML. Converting HTML → Markdown → HTML will not reproduce the original byte-for-byte. The conversion is lossy on purpose — the goal is clean, portable, human-editable text, not a faithful round-trip. To go back the other way, use our
Markdown to HTML converter .
**Q: Can I feed the Markdown to an LLM or ChatGPT?**
A: Yes — this is one of the best modern uses. Raw HTML wastes tokens on tags, attributes, scripts, and styling that a model does not need, and the noise can degrade retrieval quality in a RAG pipeline. Converting a page to Markdown strips that overhead while keeping the structure a model reads well: headings become hierarchy, lists stay lists, tables stay tables, and links stay links. The output is typically a fraction of the original HTML's token count, so you fit more real content in the context window. Paste a scraped page here, copy the Markdown, and drop it into your prompt, embedding step, or document store.
**Q: Are my files uploaded to a server?**
A: No. The conversion runs entirely in your browser: the HTML is parsed into a DOM and serialised to Markdown locally with JavaScript, and nothing is transmitted, stored, or logged. You can confirm it by opening your browser's Network tab — converting triggers zero network requests. That makes the tool safe for internal CMS exports, unpublished pages, customer content, and anything under NDA. There is no upload step and no size limit beyond what your browser can comfortably hold in memory.
**Q: Does it work offline?**
A: Yes, once the page has loaded. The DOM parser and the Markdown serialiser both run in the browser with no server round-trip, so you can convert with your network disconnected — on a plane, behind a strict firewall, or any time you would rather a page never left your machine. This falls straight out of the privacy-first design: because nothing is sent anywhere, there is nothing the tool needs the network for after the initial load.
**Q: Can I convert Markdown back to HTML?**
A: Yes. Switch to the Markdown → HTML tab, or open the dedicated
Markdown to HTML converter , paste your Markdown, and get rendered HTML with a live preview, full GFM support, and fragment, full-document, or email-inline output. The two directions pair up: use HTML → Markdown to pull existing web content into a Markdown workflow, and Markdown → HTML to publish or preview it. If the source HTML is messy, our
HTML Formatter can tidy it before you convert.
---
### htpasswd Generator — bcrypt, Apache MD5 (apr1) & Basic Auth
URL: https://go-tools.org/tools/htpasswd-generator
Generate htpasswd entries with bcrypt, Apache MD5 (apr1), SHA-1 & more. Get ready-to-paste Apache, nginx & Docker config. 100% in your browser — no upload.
#### What Is an htpasswd File?
An
.htpasswd file stores the credentials used by HTTP Basic Authentication. Each line is a single
username:hash pair, where the hash is a one-way digest of the password — the plaintext is never stored. Web servers read this file to decide who may access a protected URL. On Apache, a
.htaccess file (or a
<Directory> block) references the
.htpasswd file and prompts the browser for a username and password before serving the page.
The hash format depends on which algorithm produced it. Apache's
htpasswd tool can emit several:
bcrypt (lines starting with
$2y$) is the strongest and is recommended for Apache, Docker Registry, and Caddy;
apr1 (Apache MD5, starting with
$apr1$) is the most portable and the safe default for nginx;
SHA-1 (starting with
{SHA}) is unsalted and considered insecure;
crypt (traditional DES) is legacy and truncates at 8 characters; and
plain stores the password in cleartext, which should never be used in production.
This generator runs entirely in your browser — no username, password, or hash is ever uploaded. If you need a strong password to go with your entry, use our
Random Password Generator . To build the
Authorization: Basic header by hand, the credential is just
base64(user:password), which you can produce with our
Base64 Encoder . And once your endpoint is protected, test it from the command line with our
cURL Command Builder .
```
# Apache htpasswd CLI equivalents (apache2-utils / httpd-tools)
# bcrypt entry, printed to stdout (recommended; -B = bcrypt, -n = no file, -b = password on CLI)
htpasswd -Bbn admin 's3cret'
# → admin:$2y$10$N9qo8uLOickgx2ZMRZoMye...
# apr1 (Apache MD5) entry, portable for nginx — no apache2-utils needed
printf "admin:$(openssl passwd -apr1 's3cret')\n"
# → admin:$apr1$k3l4Hj9.$qN8...
# Append a user to an existing file from the shell
htpasswd -B /etc/apache2/.htpasswd alice
# Note: nginx delegates bcrypt to the system crypt(); on Alpine/musl or old
# glibc that fails — prefer apr1 for nginx to stay portable.
```
#### FAQ
**Q: bcrypt vs apr1 — which should I choose?**
A: Use bcrypt for Apache, Docker Registry, Caddy, and Traefik — it's a strong, salted, adaptive hash and is the modern standard. Use apr1 (Apache MD5) for nginx, because nginx hands bcrypt off to the system crypt() and that fails on many builds, while apr1 is implemented internally and works everywhere. If you control the runtime and know bcrypt is supported, bcrypt is always the stronger choice; apr1 is about portability, not security.
**Q: Does nginx support bcrypt?**
A: Only indirectly, and not reliably. nginx doesn't hash passwords itself — for $2y$ entries it delegates verification to the C library's crypt() function, so support depends entirely on your libc. Alpine's musl and older glibc builds don't include the blowfish (bcrypt) scheme, so authentication silently fails. For portable nginx setups, use the apr1 format instead, which nginx verifies internally on every platform.
**Q: How do I fix the nginx error `crypt_r() failed (22: Invalid argument)`?**
A: That error means nginx tried to verify a bcrypt ($2y$) hash on a libc that doesn't support the blowfish scheme — typically Alpine/musl or an older glibc. The fix is to regenerate the entry as apr1 (Apache MD5) instead of bcrypt, which nginx verifies internally on any platform. Alternatively, switch to a base image whose libc includes bcrypt support, but apr1 is the simpler, portable solution.
**Q: Where should I put the .htpasswd file and what permissions?**
A: Store the .htpasswd file outside the web document root so it can never be served as a static file and exposed. A common location is /etc/apache2/.htpasswd or /etc/nginx/.htpasswd. Set permissions to 640 (chmod 640) and make it owned by the user the web server runs as (for example www-data or nginx), so the server can read it but other accounts cannot.
**Q: How do I configure Basic Auth in .htaccess / nginx?**
A: For Apache, this tool generates a .htaccess block with AuthType Basic, AuthName, AuthUserFile pointing at your .htpasswd path, and Require valid-user. For nginx, it generates a location block with auth_basic "Restricted"; and auth_basic_user_file /path/.htpasswd;. Copy the config block that matches your server, adjust the file path, and reload — the snippets are ready to paste.
**Q: Are my passwords uploaded anywhere?**
A: No. Every hash is computed entirely in your browser using JavaScript — no username, password, or generated hash is ever sent over the network. You can confirm this by opening your browser's Developer Tools (F12 → Network tab) while generating: there are zero outgoing requests. Nothing is stored or logged on any server, so it's safe to generate real production credentials here.
**Q: What's the difference between $2a$, $2b$, and $2y$ in bcrypt?**
A: They are version prefixes for the same bcrypt algorithm and produce equivalent hashes; the differences trace back to historical bug fixes in how certain implementations handled high-bit characters and string length. Apache's htpasswd emits $2y$. Modern bcrypt libraries treat $2a$, $2b$, and $2y$ as interchangeable for verification, so a $2y$ entry generated here will validate correctly in Apache, Caddy, Traefik, and Docker Registry.
**Q: What bcrypt cost should I use?**
A: Cost 12 is the modern default and a good balance of security and speed. The cost is a work factor: each increment doubles the time to compute and verify the hash, which slows down brute-force attacks but also adds latency to every login. Cost 10 is acceptable for low-traffic or low-risk endpoints; 12–14 is recommended for anything sensitive. Avoid going so high that legitimate authentication becomes noticeably slow.
**Q: htpasswd vs the Authorization: Basic header — what's the difference?**
A: They sit on opposite ends of the same exchange. The .htpasswd file holds the server-side stored hash — a one-way digest the server uses to verify credentials. The Authorization: Basic header is the client-side request credential: the literal base64 of username:password the browser or curl sends on each request. The server base64-decodes the header, then checks the password against the stored hash. One is storage, the other is transport.
**Q: I don't have apache2-utils installed — how do I generate an htpasswd entry?**
A: You don't need it — this tool generates valid bcrypt, apr1, and SHA-1 entries entirely in your browser. If you prefer the command line, OpenSSL ships on almost every system: run openssl passwd -apr1 to produce an apr1 hash, then prefix it with username: to form the line. On Debian/Ubuntu you can also install the htpasswd binary via apt install apache2-utils, or httpd-tools on RHEL/CentOS.
**Q: What do the htpasswd flags -B, -Bbn, -bnB mean?**
A: Each letter is an independent flag: -B selects bcrypt, -n prints the result to stdout instead of writing a file, and -b takes the password as a command-line argument (rather than prompting). The order doesn't matter, so -Bbn and -bnB are identical. -Bbn is the common combination for piping a bcrypt entry into a Docker Registry htpasswd file.
**Q: Why does Docker Registry require bcrypt?**
A: The Docker Registry's htpasswd authentication backend only accepts bcrypt-formatted entries; apr1, SHA-1, and crypt hashes are rejected and login will fail. Generate the entry with htpasswd -Bbn user password (or use the bcrypt option here), mount the file into the registry container, and point REGISTRY_AUTH_HTPASSWD_PATH at it. Always pair this with TLS, since Basic Auth credentials are otherwise readable in transit.
**Q: Is Basic Auth secure?**
A: Only over HTTPS. HTTP Basic Auth sends credentials as base64(username:password) on every request, and base64 is reversible encoding — not encryption — so anyone who can read the traffic can recover the password instantly. Over TLS the header is encrypted in transit and Basic Auth is acceptable for simple gating. Never use it on plain HTTP, and prefer stronger schemes for high-value applications.
---
### IEEE 754 Floating-Point Converter
URL: https://go-tools.org/tools/ieee-754-converter
Convert floats to IEEE 754 hex & binary, or decode hex to float — FP16, FP32, FP64 and bfloat16. See the exact stored value, rounding error and bit layout. 100% in your browser.
#### What Is IEEE 754 Floating Point?
IEEE 754 is the standard that defines how computers store real numbers in binary. Every value is packed into three fields: a sign bit, an exponent (stored with a bias so it can represent both large and tiny magnitudes), and a mantissa holding the significant digits. Almost every CPU, GPU, and programming language uses it, which is why the same rounding surprises appear in JavaScript, Python, C, and SQL alike.
The key insight is that binary floating point can only represent numbers of the form m × 2ⁿ. Decimal fractions like 0.1 are infinite repeating fractions in base 2 — 0.000110011001100… — so the format stores the nearest representable neighbor instead. In single precision that neighbor is 0.100000001490116119384765625; in double precision it is 0.1000000000000000055511151231257827021181583404541015625. Neither is 0.1. Every downstream oddity — 0.1 + 0.2 ≠ 0.3, sums that drift, equality checks that fail — follows from this single fact, and this converter makes it visible by printing the exact stored value rather than a rounded-back approximation. The same drift shows up whenever floats round-trip through JSON — inspect the payload with the
JSON formatter and the bits here.
The standard also reserves bit patterns for special values. An all-ones exponent encodes ±Infinity (mantissa zero) or NaN (mantissa non-zero); an all-zeros exponent encodes signed zero (mantissa zero) or subnormal numbers (mantissa non-zero), which fill the underflow gap next to zero at reduced precision. The four formats this tool covers — binary16/FP16, bfloat16, binary32/FP32, binary64/FP64 — differ only in how many bits they give each field: more exponent bits mean more range, more mantissa bits mean more precision. bfloat16, the machine-learning favorite, is simply FP32 with the bottom 16 mantissa bits cut off: same range, much coarser precision.
A useful mental model is that representable floats form a grid on the number line whose spacing — one unit in the last place, or ULP — doubles at every power of two. Near 1.0 a double's grid spacing is about 2.22 × 10⁻¹⁶; near 2⁵³ it is a whole integer, which is why doubles cannot count beyond 2⁵³ reliably. The neighbors panel in this tool shows that grid directly: the previous and next representable values around whatever you type, with the exact gap between them.
```
// Float ↔ hex through the raw IEEE 754 bits (works in any browser / Node.js)
const buf = new DataView(new ArrayBuffer(8));
function floatToHex32(value) {
buf.setFloat32(0, value); // rounds to nearest even
return '0x' + buf.getUint32(0).toString(16).toUpperCase().padStart(8, '0');
}
function hexToFloat32(hex) {
buf.setUint32(0, parseInt(hex, 16));
return buf.getFloat32(0);
}
floatToHex32(0.1); // '0x3DCCCCCD'
floatToHex32(3.14159); // '0x40490FD0'
hexToFloat32('3DCCCCCD'); // 0.10000000149011612
```
#### FAQ
**Q: Why is 0.1 + 0.2 not equal to 0.3?**
A: Because 0.1, 0.2 and 0.3 have no exact binary representation — a double stores the nearest representable value instead. 0.1 and 0.2 are each stored slightly high, so their sum lands one bit above the stored 0.3: bit pattern 0x3FD3333333333334 versus 0x3FD3333333333333, which is why 0.1 + 0.2 == 0.3 is false in JavaScript, Python, Java, C, Rust and Go. The double behind 0.1 is exactly 0.1000000000000000055511151231257827021181583404541015625, and the sum stored for 0.1 + 0.2 is 0.3000000000000000444089209850062616169452667236328125 — type 0.1 into this converter to read the stored value digit for digit.
**Q: What is IEEE 754?**
A: IEEE 754 is the technical standard for binary floating-point arithmetic used by virtually every modern CPU, GPU, and programming language. It defines how a number is packed into bits — one sign bit, an exponent field, and a mantissa (significand) — plus the rounding rules and the special values Infinity, NaN, signed zero, and subnormals. The formats you will meet in practice are binary32 (float, FP32), binary64 (double, FP64), binary16 (half, FP16), and the related bfloat16 truncation used in machine learning. This converter shows all four at the bit level.
**Q: What is the difference between FP16 and bfloat16?**
A: Both are 16-bit formats, but they split their bits differently. FP16 (IEEE binary16) uses 5 exponent bits and 10 mantissa bits: more precision, but a tiny range — the largest finite value is 65504, so overflow to Infinity is a constant hazard. bfloat16 keeps FP32's 8 exponent bits and only 7 mantissa bits: the full ±3.4 × 10³⁸ range of a float, with much coarser precision. That is why ML training, where gradients can spike far beyond 65504, standardized on bfloat16, while FP16 suits storage and inference where values are tamed. Compare them here: type 0.1 and switch formats — FP16 stores 0.0999755859375 (0x2E66), bfloat16 stores 0.10009765625 (0x3DCD).
**Q: What are subnormal (denormal) numbers?**
A: When the exponent field is all zeros, IEEE 754 drops the implicit leading 1 and lets the mantissa shrink gradually toward zero — these are subnormal (older term: denormal) numbers. They fill the gap between zero and the smallest normal number, so the difference of two unequal floats can never round to zero (gradual underflow). The cost is reduced precision and, on many CPUs, slower arithmetic. In FP16 the smallest subnormal is 2⁻²⁴ = 0.000000059604644775390625; click the Min subnormal chip in any format to inspect its bit pattern — exponent bits all zero, mantissa 0…001.
**Q: Should I use float or double?**
A: Default to double (FP64) unless you have a measured reason not to. A double carries about 15–16 significant decimal digits versus roughly 7 for a float, and most languages (JavaScript numbers, Python floats) are double-only anyway. Choose float (FP32) when memory bandwidth or storage dominates — large arrays, GPU pipelines, graphics — and you have confirmed 7 digits are enough. For money, use integers (cents) or a decimal type instead: no binary format stores 0.1 exactly, as the stored-value panel of this tool demonstrates. If you need to inspect integer bases instead, see the
number base converter .
**Q: How do I convert a float to hex by hand?**
A: Take the sign (0 for positive, 1 for negative). Write the absolute value in binary and normalize it to 1.xxx × 2ⁿ. Add the format's bias to n (127 for FP32, 1023 for FP64) and write that in the exponent bits. Drop the leading 1 and keep the next 23 (or 52) bits of the fraction as the mantissa, rounding to nearest even at the cut. Concatenate sign, exponent, mantissa and group each 4 bits into one hex digit. For 3.14159 in FP32 that yields 0 | 10000000 | 10010010000111111010000 → 0x40490FD0 — the same steps with bias 1023 and 52 mantissa bits turn a double to hex — or skip the arithmetic and let this converter do each step visibly.
**Q: Is my data uploaded when I use this converter?**
A: No. Every conversion runs locally in your browser using plain JavaScript — DataView and BigInt arithmetic, no server round-trip, no third-party libraries. You can open your browser's developer tools, watch the network panel stay silent while you type, or disconnect from the internet entirely and keep converting. The Copy link button encodes the bit pattern into the URL fragment, which is likewise never sent to any server.
---
### Compress Images Online — JPEG, PNG & WebP
URL: https://go-tools.org/tools/image-compressor
Compress JPEG, PNG, WebP & AVIF up to 80% smaller — in your browser, no upload. Batch 20 images, resize, compare before & after, download as ZIP. Free & private.
#### What Is Image Compression?
Image compression reduces file size by removing redundant or imperceptible visual data, enabling faster page loads and reduced bandwidth consumption. According to the HTTP Archive Web Almanac, images account for approximately 50% of total page weight on average — making image optimization one of the highest-impact performance improvements available to web developers.
As Google's web performance guidance notes, optimizing images is consistently among the top recommendations from Lighthouse and PageSpeed Insights, directly improving Core Web Vitals metrics such as Largest Contentful Paint (LCP). The WebP specification (Google, 2010) demonstrated that modern compression algorithms can reduce image file sizes by 25–35% compared to JPEG at equivalent visual quality, a finding that has since driven widespread adoption of next-generation formats (HTTP Archive, WebP specification).
There are two main compression approaches:
**Lossy compression** discards some image data to achieve smaller file sizes. JPEG and WebP use lossy compression by default — a quality setting of 75% typically reduces file size by 60–80% with minimal visible difference. The tradeoff is irreversible: once data is discarded, it cannot be recovered from the compressed file.
**Lossless compression** reduces file size without discarding any data. PNG uses lossless compression by default — the decompressed image is bit-for-bit identical to the original. The compression ratio is lower (typically 10–30%), but image quality is perfectly preserved.
This tool compresses your images entirely in your browser — your images are never uploaded to any server, at any point. For JPEG and WebP files, the quality slider directly controls the lossy compression level through the Canvas API. PNG files come back as PNG: because the Canvas API cannot encode a lossy PNG, the tool compresses them with palette quantization instead, which trims the number of distinct colors and keeps transparency intact, including soft anti-aliased edges. If you want a different container, the output format selector lets you request PNG or WebP for any input. Every compression operation stays on your device, giving you the performance gains without the privacy cost.
For embedding small compressed images directly in HTML or CSS, you can
Base64-encode the output to create data URIs — a common technique for icons and logos under 5 KB. For a deeper comparison of browser-based vs Node.js compression solutions — including Squoosh, Sharp, and Imagemin — read our
image compression guide .
```
// Compress a JPEG image in the browser using the Canvas API
async function compressImage(file, quality = 0.75) {
const img = await createImageBitmap(file); // decode the image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0);
// quality: 0.0 (smallest file) → 1.0 (original quality)
return new Promise((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', quality)
);
}
// file comes from an
or drag-and-drop
const blob = await compressImage(file, 0.75);
console.log(`Original: ${file.size} bytes`);
console.log(`Compressed: ${blob.size} bytes`);
// → Original: 2100000 bytes
// → Compressed: 672000 bytes (~68% reduction)
```
#### FAQ
**Q: Is it safe to compress images online?**
A: Yes — this tool is completely safe because it processes images entirely in your browser. Your images are never uploaded to any server. The compression uses the browser's built-in Canvas API, and all data stays on your device. You can verify this by opening your browser's Network tab in Developer Tools — you will see zero network requests during compression. When you close or refresh the page, all image data is cleared from memory.
**Q: What is the difference between lossy and lossless compression?**
A: **Lossy compression** permanently removes some image data to achieve smaller file sizes. JPEG and WebP use lossy compression — a quality setting of 75 typically reduces file size by 60–80% with minimal visible difference, but the removed data cannot be recovered.
**Lossless compression** reduces file size without removing any data. The decompressed image is bit-for-bit identical to the original. PNG uses lossless compression. The tradeoff is that lossless compression achieves smaller reductions (typically 10–30%).
For web use, lossy compression at quality 75–85 is almost always the right choice — the file size savings are dramatic and the quality difference is imperceptible to most viewers.
**Q: Do my PNG files stay PNG?**
A: Yes. A PNG you upload comes back as a PNG. The browser's Canvas API cannot encode a lossy PNG, so rather than change the format, this tool quantizes the image to a color palette — it reduces the number of distinct colors and writes a real PNG back out, with transparency preserved, including soft anti-aliased edges.
If you do want a WebP, pick **WebP** in the output format selector above the file list. **Same as input** is the default and leaves every file in its own format; **PNG** forces PNG output even for JPEG or WebP input.
At quality 100 the PNG is re-encoded losslessly, so no pixels change and the size reduction depends entirely on how efficiently your original was already encoded — it can be zero. Whenever the result is not smaller than the file you uploaded, the tool discards it and keeps your original.
**Q: What quality setting should I use?**
A: It depends on your use case:
- **Quality 85–95**: Visually indistinguishable from the original. Use for professional photography, portfolio sites, or anywhere image quality is critical. Typical reduction: 30–50%.
- **Quality 70–85**: Excellent quality with significant size savings. The recommended range for most web use. Typical reduction: 50–75%.
- **Quality 50–70**: Good quality with aggressive compression. Suitable for thumbnails, social media, and images viewed at small sizes. Typical reduction: 70–85%.
- **Quality below 50**: Noticeable artifacts. Only use when file size is more important than quality (e.g., email constraints, very low bandwidth).
Use the Compare button to find the lowest quality that looks acceptable for your specific image.
**Q: Can I compress images without losing quality?**
A: Technically, yes — set the quality slider to 100 for lossless compression. However, the file size reduction will be minimal (0–10% for most images) because lossless compression can only remove redundant encoding data, not image data.
In practice, quality 80–85 is effectively "no visible quality loss" for most images. The human eye cannot distinguish between quality 85 and quality 100 in typical viewing conditions. The Compare slider lets you verify this for your specific image.
For maximum file size reduction without visible quality loss, start at quality 75 and use the Compare button to check. If you see artifacts, increase the quality in increments of 5 until the result looks acceptable.
**Q: How many images can I compress at once?**
A: You can compress up to 20 images in a single batch. Each image can be up to 10MB in size. All processing happens in your browser, so performance depends on your device's CPU and available memory.
For large batches of high-resolution images, compression may take a few seconds. The tool processes all images and shows a per-file progress indicator and the total space saved.
**Q: What happens if the compressed file is larger than the original?**
A: This can happen with images that are already well-optimized, or when compressing at very high quality settings (90–100). The tool then keeps your original file and shows "0% saved", with a tooltip explaining why nothing was replaced.
If this happens, the original image was likely compressed with an advanced encoder (like mozjpeg, cjpeg, or pngquant) that is more efficient than the browser's built-in encoder. In this case, your original file is already optimally compressed — no further action is needed.
**Q: Does compression change my image dimensions?**
A: By default no — pixel dimensions are preserved. A 4000×3000 image stays 4000×3000 after compression and only the file size changes.
If you want to resize, set a max width in pixels and keep the **Keep aspect ratio** checkbox enabled. The image is downscaled (never upscaled) and height is derived from the original aspect ratio. Resizing and compression compound — a 4000 px photo dropped to 1600 px max width before quality 75 compression often shrinks 5–10× total.
**Q: What image formats are supported?**
A: This tool supports four formats:
- **JPEG** (.jpg, .jpeg): The most common format for photographs. Supports lossy compression with the quality slider.
- **PNG** (.png): Best for graphics with transparency. Compressed by palette quantization and saved as PNG, with transparency preserved.
- **WebP** (.webp): Modern format with the best compression efficiency. Supports both lossy compression and transparency.
- **AVIF** (.avif): Next-generation format with the best compression ratios in 2026. AVIF input is supported on every modern browser; AVIF encode (re-saving as AVIF) requires Chrome 85+ — on browsers without AVIF encode, AVIF input is transparently re-encoded as WebP.
Other formats (GIF, SVG, HEIC, TIFF) are not currently supported.
**Q: How does this compare to TinyPNG or Squoosh?**
A: The main difference is **privacy**: this tool processes images entirely in your browser — your files never leave your device. TinyPNG uploads images to their servers for processing.
**TinyPNG** uses server-side compression with advanced algorithms (pngquant for PNG, mozjpeg for JPEG) that can produce slightly smaller files than browser-based compression. However, your images must be uploaded to their servers, and the free tier limits you to 20 images per day at 5MB each.
**Squoosh** (by Google) also processes images in the browser using WebAssembly, offering more codecs and finer control. This tool is simpler and faster for the common case of batch-compressing JPEG, PNG, and WebP files with a single quality setting.
Choose this tool when privacy is a priority, you need quick batch compression, and you don't need advanced codec options.
---
### Image to Base64 Converter
URL: https://go-tools.org/tools/image-to-base64
Convert images to Base64 data URIs in your browser — PNG, JPG, GIF, WebP, SVG, ICO. Copy HTML, CSS, Markdown & JSON, with the exact size increase. 100% private, no upload.
#### What is a Base64 Image (Data URI)?
A Base64 image is a picture whose binary bytes have been re-encoded as a string of printable ASCII characters using the Base64 alphabet (A–Z, a–z, 0–9, + and /). Wrapped in the data: URI scheme — data:image/png;base64,iVBORw0KGgo… — that string can appear anywhere a URL is expected: an HTML img src, a CSS background-image, an email body, or a field inside a JSON payload. The browser decodes it on the fly and displays the image with no separate network request. This is why Base64 images are sometimes called "inline" or "embedded" images.
The encoding exists for a simple reason: many systems were built to carry text, not arbitrary binary. HTML, JSON, email headers, and URLs all expect characters, and raw image bytes would include control codes and delimiters that break them. Base64 maps every 3 binary bytes onto 4 safe text characters, guaranteeing the data survives transport intact. The cost is size: the text representation is about 33% larger than the original binary, and it cannot be cached independently of the document that contains it.
That trade-off defines when Base64 images make sense. For a tiny icon used in one stylesheet, inlining removes a round trip and the size penalty is negligible — a clear win. For a 200 KB hero photo reused across every page, inlining bloats every page, defeats the browser cache, and costs CPU to decode on each load — a clear loss. The modern, HTTP/2-era guidance is to inline only small, stable assets and serve everything else as ordinary cached files. This tool surfaces the exact numbers for your image and a traffic-light recommendation so the decision is grounded in data, not folklore.
The reverse operation — turning a Base64 string back into a viewable, downloadable image — is equally useful when you are debugging a data URI from a stylesheet, inspecting an API response, or recovering an asset embedded in a config file. Switch to the Base64 → Image tab or open the dedicated
Base64 to Image decoder .
```
/* CSS */
.badge {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==");
}

// JSON
{ "mime": "image/png", "data": "iVBORw0KGgo…" }
```
#### FAQ
**Q: What does this Image to Base64 converter do?**
A: It reads an image you drop, paste, or select and encodes its bytes as a Base64 string — entirely inside your browser. You get the raw Base64, a ready-to-use data URI (data:image/png;base64,…), and copy-paste snippets for HTML
, CSS background-image, Markdown, and JSON. A metadata bar reports the original file size, the encoded size, the exact percentage increase (Base64 is about 33% larger), the pixel dimensions, and the MIME type. Nothing is uploaded: the encoding runs locally via the FileReader API, so the same tool is safe for screenshots, internal assets, and unreleased artwork. To go the other way, use the Base64 → Image tab or our
Base64 to Image decoder .
**Q: Are my images uploaded to a server?**
A: No. Every step happens client-side in your browser using the FileReader API and JavaScript string encoding. Your image is never transmitted, never stored, and never logged. You can confirm this by opening your browser's Network tab — encoding an image triggers zero network requests. This makes the tool safe for sensitive material: product screenshots before launch, internal diagrams, customer assets, and anything under NDA. There is no file-size cap imposed by an upload limit, only the practical limit of how large a Base64 string your browser and target system can comfortably handle.
**Q: How much bigger does Base64 make an image?**
A: Base64 encodes every 3 bytes of binary data as 4 ASCII characters, so the encoded string is roughly 33% larger than the original file (plus a few bytes of padding and the data: prefix). A 9 KB PNG becomes about 12 KB of text. This overhead is the single most important reason not to Base64 large images: you ship more bytes and, because the string is embedded in your HTML or CSS, those bytes are re-downloaded every time the containing file changes and cannot be cached independently. The tool shows the exact increase for your specific file in the metadata bar so you can make the call with real numbers.
**Q: When should I use a Base64 image instead of a normal file?**
A: Base64 (as a data URI) is a good fit for small, rarely-changing assets where avoiding a separate HTTP request matters more than caching: tiny icons and logos inlined in CSS, images embedded in HTML email (many clients block external images but render data URIs), single-file widgets or bookmarklets that must be self-contained, SVG sprites, and images stored inside JSON/API payloads. A practical rule of thumb: under about 2 KB and used on one or two pages, inlining usually wins. The advice badge in this tool encodes exactly that heuristic — green under 2 KB, amber up to 10 KB, red above.
**Q: When should I NOT use Base64 images?**
A: Avoid Base64 for anything large or reused across pages. Four concrete reasons: (1) the ~33% size increase means more bytes over the wire; (2) an inlined image cannot be cached on its own — it is re-downloaded with every change to the HTML or CSS that contains it, and repeated on every page that embeds it; (3) decoding a large data URI costs CPU and battery, which is noticeable on mobile; and (4) you lose responsive images (srcset/sizes) and lazy-loading. Since HTTP/2 multiplexes many small requests cheaply, the original reason to inline — cutting request count — rarely applies anymore. For photos, hero images, or anything over ~10 KB, a normal cached file almost always loads faster. If the goal is a smaller file, run it through our
Image Compressor first.
**Q: How do I use the Base64 output in HTML and CSS?**
A: For HTML, switch to the HTML tab and paste the generated element:
. For CSS, use the CSS tab, which wraps the data URI in background-image: url("data:image/png;base64,…"). Both work anywhere a URL is accepted — img src, CSS background, mask-image, even favicon link tags. The data: scheme is supported by every modern browser. One caveat: very long data URIs in inline HTML can hurt readability and, in CSS, bloat the stylesheet that ships to every visitor, so reserve inlining for genuinely small assets.
**Q: Which image formats are supported?**
A: PNG, JPEG/JPG, GIF (including animated), WebP, SVG, ICO, and BMP are all supported, plus AVIF where the browser can decode it. Because the tool encodes the raw bytes rather than re-rendering the image, animated GIFs stay animated, transparent PNGs keep their alpha channel, and SVGs remain fully scalable. The MIME type is read from the file itself and, when you paste raw Base64 into the decoder, inferred from the data's magic bytes. There is no format conversion during encoding — the output represents exactly the file you provided.
**Q: Why is SVG a special case?**
A: SVG is XML text, not binary, so Base64 actually makes it larger and harder to read for no benefit. For inlining SVG in CSS or HTML, URL-encoding the markup (percent-encoding a handful of characters like #, <, >, and quotes) is usually smaller than Base64 and keeps the source legible and gzip-friendly. This tool still offers Base64 SVG output because some pipelines require it, but if you are hand-optimizing CSS, prefer a URL-encoded data URI. Our
URL Encoder/Decoder helps with that approach.
**Q: Is Base64 the same as encryption?**
A: No. Base64 is an encoding, not encryption — it is fully reversible by anyone with no key required. It exists to represent binary data using a safe set of printable ASCII characters so the data survives transport through systems that only handle text (HTML, JSON, email headers, URLs). Anyone can decode a Base64 string back to the original image in seconds, including with the Base64 → Image tab here. Never treat Base64 as a way to hide or protect sensitive image content; it provides zero confidentiality.
**Q: Can I embed a Base64 image in an email?**
A: Yes, and it is one of the better uses of the technique. Many email clients block externally hosted images by default for privacy, which breaks layouts that rely on remote logos. Embedding small images as data URIs ensures they render immediately without a server fetch. The trade-offs: some older clients (notably certain versions of Outlook) have spotty data-URI support, and large embeds inflate the message size that every recipient downloads. Keep embedded images small — logos and icons, not photographs — and test across your target clients.
**Q: Why does my Base64 image not render?**
A: The most common causes: a missing or wrong MIME type in the data: prefix (use image/png for PNG, image/jpeg for JPG, image/svg+xml for SVG), whitespace or line breaks accidentally inserted into the string, a truncated copy that dropped the trailing padding (= or ==), or pasting only the raw Base64 without the data:…;base64, prefix where a URL is expected. The decoder in this tool is tolerant — it strips whitespace, accepts input with or without the prefix, and infers the MIME from the image's magic bytes — so pasting your string into the Base64 → Image tab is the fastest way to confirm whether the data itself is valid.
---
### JavaScript Formatter & Minifier
URL: https://go-tools.org/tools/js-formatter
Format, beautify and minify JavaScript instantly in your browser. Clean up messy code or compress it with Terser to ship — free, private, and your code never leaves your device.
#### What is JavaScript Formatting?
JavaScript formatting (also called beautifying or pretty-printing) rewrites code with consistent indentation, spacing and line breaks so it's easy to read and review. The code behaves identically before and after — only whitespace changes. Minifying does the reverse: it shortens names, drops comments and collapses whitespace to produce the smallest bundle that runs the same. This tool does both, entirely in your browser.
#### FAQ
**Q: How do I format JavaScript online?**
A: Paste your code into the input box and click Format. The tool reindents it with consistent spacing and line breaks, then lets you copy it. Everything runs locally in your browser — nothing is uploaded.
**Q: How do I minify JavaScript?**
A: Paste your code and click Minify. The tool runs Terser to rename locals, remove comments and collapse whitespace into the smallest equivalent script, and shows how many bytes you saved.
**Q: What is the difference between formatting and minifying JavaScript?**
A: Formatting (beautifying) adds indentation and spacing to make code readable. Minifying shortens names and strips whitespace and comments to shrink the bundle for faster loading. Both run with the same behavior as the original.
**Q: Does minifying change what my code does?**
A: No. Terser preserves behavior — it only renames local variables and removes whitespace, comments and unreachable code. The minified script runs the same as the source.
**Q: Is my code safe with this tool?**
A: Yes. All formatting and minifying happen locally in your browser using JavaScript — your code is never sent to any server, logged, or stored. That makes it safe for proprietary or unreleased code, unlike server-side tools that receive a copy of everything you paste.
**Q: Why did minify report an error?**
A: Terser needs syntactically valid JavaScript. If you paste an incomplete snippet or TypeScript/JSX, parsing fails — format works on a best-effort basis, but minification requires valid JS. Fix the syntax or transpile first, then try again.
**Q: What indentation should I use for JavaScript?**
A: Two spaces is the most common default in modern JavaScript and keeps diffs compact; four spaces and tabs are also widely used. Pick one and apply it consistently — this tool supports all three when beautifying.
---
### JSON Diff & Compare
URL: https://go-tools.org/tools/json-diff
Compare two JSON files instantly in your browser. Side-by-side highlighting, RFC 6902 JSON Patch output, ignore noisy fields like timestamps and IDs. 100% private, no upload.
#### What is JSON Diff?
JSON Diff is a structural comparison of two JSON documents that respects JSON's data model — keys are unordered, types are strict, and arrays may be ordered or keyed. Unlike a text diff (which compares lines and reports key reorders or whitespace as differences), a JSON diff produces semantically meaningful results.
The canonical machine-readable form is JSON Patch (RFC 6902), an ordered ops array (add, remove, replace, move, copy, test) that transforms one document into another. Paths use JSON Pointer (RFC 6901). Closely related: JSON Merge Patch (RFC 7396) — simpler but cannot distinguish 'remove key' from 'set key to null'. This tool outputs RFC 6902.
Deep equality on JSON in JavaScript is harder than it looks. JSON.stringify(a) === JSON.stringify(b) fails on key reorder, misleads on -0 vs 0 (both stringify to "0"). A correct diff must walk both trees in parallel using key-set union, distinguish null from missing via the 'in' operator, and decide what 'equal' means for numbers (Object.is by default, epsilon for tolerance).
This tool runs entirely in your browser. Inputs never leave your machine. Safe for API responses, internal schemas, and proprietary configs.
Working with adjacent JSON tools? Format with
JSON Formatter ; convert with
JSON to YAML ,
YAML to JSON ,
JSON to CSV , and
CSV to JSON . See
our guide for advanced timestamp/ID filtering patterns. Need to validate the structure (not just diff it)? See our
JSON Schema validation guide .
```
// Two JSON documents that look different but are semantically equal
const a = '{"a":1,"b":2}';
const b = '{"b":2,"a":1}';
// Naive comparison — wrong
JSON.stringify(JSON.parse(a)) === JSON.stringify(JSON.parse(b));
// → false (key order differs)
// JSON Diff (this tool) — correct: key order is irrelevant
// → 0 differences
// JSON Patch (RFC 6902) for { "a": 1 } → { "a": 2 }
// [{ "op": "replace", "path": "/a", "value": 2 }]
```
#### FAQ
**Q: Why does my diff show everything changed when I only changed one field?**
A: Three usual suspects: (1) different key order — JSON Diff treats key order as equivalent, but text diff tools don't; (2) timestamps/UUIDs/auto-IDs that mutate on every request — add them to Ignore paths; (3) array order, when by-index comparison shouldn't apply — switch Array mode to 'Match by key'.
**Q: How do I ignore timestamps and IDs in JSON diff?**
A: Use the Ignore paths input above. Click the 'Timestamps' or 'IDs' preset for one-click filtering of /createdAt, /updatedAt, /*Id, /*At, /requestId. You can also paste your own Extended JSON Pointer patterns — one per line — for advanced filtering.
**Q: What's the difference between JSON Patch and a visual diff?**
A: Visual (side-by-side) diff is for humans — review changes by eye. JSON Patch (RFC 6902) is for machines — a structured ops array (add/remove/replace) you can apply with fast-json-patch or rfc6902 npm packages. Same diff, two outputs.
**Q: Does JSON diff treat null and missing keys the same?**
A: No. {"a":null} and {} differ — the first has an explicit null, the second has no key. Real systems behave differently for the two; this tool keeps them distinct.
**Q: How are arrays compared — by index or by key?**
A: By index (Sequential) by default. Switch to 'Match by key' and provide a key field (commonly id) to align elements regardless of order. Use this for K8s envs, package-lock entries, or any list that's logically a set.
**Q: Can I export the diff as RFC 6902 JSON Patch?**
A: Yes. The JSON Patch tab outputs a valid RFC 6902 ops array. If Ignore paths are set, the patch is filtered (the tab shows '(filtered: excludes N ignored paths)') and will not round-trip the originals exactly. Clear Ignore paths for a complete patch.
**Q: Is JSON Patch the same as JSON Merge Patch (RFC 7396)?**
A: No. RFC 6902 (JSON Patch) is an ordered ops array — explicit and reversible. RFC 7396 (Merge Patch) is a single merge document — simpler but cannot represent removal differently from setting null. JSON Diff outputs RFC 6902.
**Q: How do I compare two large JSON files (>10 MB)?**
A: Files over ~5 MB exceed practical browser memory. Live mode disables at 200 KB; for multi-megabyte files, use command-line jq or fast-json-patch in Node.
**Q: Does the tool send my JSON to a server?**
A: No. All comparison runs locally in your browser. Your JSON inputs are never written to disk, network, localStorage, or URL parameters. Only your preferences (ignore paths, array mode, numeric tolerance, active tab) are stored in localStorage so they persist across sessions. Refreshing the page clears the JSON inputs. The Share Link button writes only your config (Array mode, Ignore paths) — never your data.
**Q: Why does 42 differ from "42" in the diff?**
A: JSON Diff is type-strict: number 42 and string "42" are not equal. This catches backend serialization drift (some endpoints return numeric IDs, others return strings) — the diff labels it as 'type' modification.
**Q: Can I diff JSON with comments (JSONC) or trailing commas?**
A: Standard JSON (RFC 8259) does not allow comments or trailing commas. This tool uses native JSON.parse, which rejects both. Strip comments first using
JSON Formatter .
**Q: How do I compare nested arrays of objects by a key like id?**
A: Set Array mode to 'Match by key' and enter id. Diff aligns by id values. v1 applies the same key field at every array depth; inner arrays without that field fall back to sequential and emit a warning chip.
**Q: Does the diff handle floating-point precision (0.1 + 0.2)?**
A: Yes, with Numeric tolerance. Default is 0 with Object.is — so -0 vs +0 are flagged. Set tolerance to a small epsilon (e.g. 1e-9) and 0.1 + 0.2 will compare equal to 0.3. Tolerance applies only to numeric leaves.
---
### JSON Escape
URL: https://go-tools.org/tools/json-escape
Escape any text or JSON into a valid JSON string literal in your browser. Handles quotes, newlines, tabs, Unicode, and slashes. 100% private, no upload, instant.
#### What is JSON Escaping and When Do You Need It?
JSON escaping is the process of converting a raw string into a form that is safe to embed inside a JSON document. JSON has a small set of characters that carry structural meaning — the double quote delimits strings, the backslash starts an escape sequence — plus control characters (newlines, tabs) that are not allowed to appear literally inside a string. Escaping replaces each of these with a safe two-character sequence (\", \\, \n, \t) or a \uXXXX Unicode escape, so the resulting string parses cleanly anywhere.
You reach for JSON escaping more often than you might think. The most common case is JSON-in-JSON: a webhook envelope, a message-queue payload, or an audit log stores a request body as a string field, which means the inner JSON must be escaped before it can be assigned. Another is hand-authoring JSON config: pasting a multi-line shell script, SQL query, or code snippet into a single JSON value requires turning every newline into \n. A third is building REST request bodies by hand in tools like curl, where a quoted JSON string must be escaped to survive the shell and the HTTP layer.
This tool has three differentiators over a naive escaper. First, it is built on the exact JSON specification rules — the same logic a compliant serializer uses — so output round-trips losslessly: escape here, parse anywhere, get your bytes back. Second, the optional ASCII-safe mode converts every non-ASCII character (including astral emoji, handled as surrogate pairs) to \uXXXX for systems that cannot be trusted with UTF-8. Third, everything runs 100% in your browser — your payloads, which often contain PII, tokens, and secrets, never touch a server. To reverse the process, use our
JSON Unescape tool; to validate JSON first, see the
JSON Formatter .
```
// Input text
She said "hi"
then left.
// Escaped (Wrap on) — identical to JSON.stringify(input)
"She said \"hi\"\nthen left."
// Escaped (Wrap off) — just the body, for hand-built JSON
She said \"hi\"\nthen left.
// JSON-in-JSON
{"a":1} -> "{\"a\":1}" -> {"payload": "{\"a\":1}"}
```
#### FAQ
**Q: What does this JSON escape tool do?**
A: It converts any text — a JSON object, a code snippet, a log line, or plain prose — into a valid JSON string literal, entirely in your browser. Special characters that would break a JSON document are escaped: double quotes become \", backslashes become \\, newlines become \n, tabs become \t, carriage returns become \r, and other control characters become \uXXXX. The result is a string you can safely paste as a value inside a JSON document, a REST request body, a configuration file, or a database column. Nothing is uploaded — the conversion runs 100% client-side, so it is safe for payloads containing PII, secrets, or internal data.
**Q: What is the difference between JSON escape and JSON stringify?**
A: They describe the same core operation from two angles. JSON.stringify() in JavaScript takes a value and produces its JSON text representation; when the value is a string, that means wrapping it in double quotes and escaping the special characters inside — which is exactly JSON escaping. This tool does precisely that: with Wrap in double quotes on, the output equals JSON.stringify(yourText); with it off, you get the escaped body without the surrounding quotes, which is what you need when you are building the JSON by hand and already typed the quotes. So if you searched for json stringify online, this is the tool — it gives you both the quoted and unquoted forms.
**Q: Is my data uploaded anywhere?**
A: No. All escaping runs entirely in your browser using JavaScript — your text is never transmitted, stored, logged, or analyzed on any server. This makes the tool safe for API payloads with PII, authentication tokens, internal configuration, and production secrets. You can verify it in your browser's Network tab: typing or pasting triggers zero network requests. There are no cookies for your input and no third-party analytics that capture what you paste.
**Q: When do I need the \uXXXX (escape non-ASCII) option?**
A: JSON allows raw UTF-8, so by default an é stays an é and an emoji stays an emoji — perfectly valid and more readable. Turn on Escape non-ASCII only when a downstream system cannot be trusted with UTF-8: old SOAP/XML gateways, some logging pipelines, email headers, or source files that must stay pure ASCII. With it on, every character above U+007F becomes a \uXXXX sequence (astral characters like emoji become a surrogate pair, e.g. 😀 → \ud83d\ude00). The escaped output is byte-for-byte ASCII and decodes back to the original Unicode in any compliant JSON parser.
**Q: How do I embed a JSON object inside another JSON string (JSON-in-JSON)?**
A: Paste the inner JSON into the input, keep Wrap in double quotes on, and copy the result — it is now a single escaped string you can assign to a key in the outer document. For example {"a":1} becomes "{\"a\":1}", which you place after a colon: {"payload": "{\"a\":1}"}. This double-encoding is common in webhook envelopes, message-queue payloads, and audit logs that store a request body as a string. To reverse it and read the inner object, use our JSON Unescape tool.
**Q: What does the Escape forward slash (\/) option do?**
A: The forward slash / is a normal character in JSON and does not require escaping, so it is left alone by default. The option exists for one specific case: embedding JSON inside an HTML <script> tag, where the sequence </script> would prematurely close the tag. Escaping / to \/ turns </script> into <\/script>, which is still valid JSON but no longer a tag terminator. Enable it only when you are inlining JSON into HTML; for every other use, leave it off for cleaner output.
**Q: Does it handle newlines, tabs, and control characters correctly?**
A: Yes. The tool is built on the JSON specification's exact escaping rules: newline → \n, carriage return → \r, tab → \t, backspace → \b, form feed → \f, double quote → \", backslash → \\, and any remaining control character below U+0020 → \uXXXX. This is identical to what a compliant JSON serializer produces, so the output round-trips losslessly: escape it here, parse it anywhere, and you get your original text back byte for byte.
---
### JSON Formatter & Validator
URL: https://go-tools.org/tools/json-formatter
Format, validate and beautify JSON instantly in your browser. Free online tool with syntax validation, error detection, minify and one-click copy. 100% private.
#### What is JSON?
JSON (JavaScript Object Notation) is a lightweight, text-based data interchange format that is easy for humans to read and write, and easy for machines to parse and generate. Standardized as RFC 8259 and ECMA-404, JSON has become the universal standard for data exchange across virtually all programming languages, APIs, and web services.
As Douglas Crockford, the creator of JSON, wrote on json.org: "JSON's design goals were for it to be minimal, portable, textual, and a subset of JavaScript." This deliberate simplicity is exactly why JSON won out over XML and became the lingua franca of the modern web.
JSON supports six data types: strings (in double quotes), numbers, booleans (true/false), null, arrays (ordered lists), and objects (key-value pairs) (RFC 8259). Its simplicity and readability have made it the preferred format over XML for most modern web applications, REST APIs, and configuration files. JSON is the most popular data format for APIs, used by 86% of developers (Postman State of API Report 2023).
A JSON formatter transforms raw or minified JSON into a well-structured, indented format that makes the data hierarchy immediately visible. This is essential for debugging API responses, inspecting configuration files, and understanding complex nested data structures. Unlike XML, JSON does not support comments, attributes, or namespaces — it focuses purely on data representation (ECMA-404).
This tool runs entirely in your browser — your JSON data never leaves your device. Unlike server-based formatters, there are no uploads, no logging, and no data retention. Safe to use with API keys, production configs, and proprietary data.
JSON is frequently used with other developer tools. When debugging APIs, you may need to
decode Base64-encoded JSON payloads (such as JWT tokens), or generate
UUIDs for use as unique identifiers within JSON data structures. Working with JSON5 or JSONC config files? See our
JSON5 and JSONC formatting guide for syntax differences, tooling support, and best practices. For a deep dive on YAML's Norway problem and JSON ↔ YAML conversion, read our
YAML Norway problem & JSON-YAML differences guide , or convert directly with
JSON to YAML and
YAML to JSON . Need to compare two JSON documents and find what changed? Try our
JSON Diff . To move tabular JSON into a spreadsheet or import a CSV export back into JSON, use
JSON to CSV and
CSV to JSON .
```
// Format (pretty-print) JSON with 2-space indentation
const raw = '{"name":"Alice","age":30,"active":true}';
const parsed = JSON.parse(raw); // parse string → object
const formatted = JSON.stringify(parsed, null, 2);
console.log(formatted);
// → {
// "name": "Alice",
// "age": 30,
// "active": true
// }
// Minify JSON (strip all whitespace)
const minified = JSON.stringify(parsed);
console.log(minified);
// → '{"name":"Alice","age":30,"active":true}'
```
#### FAQ
**Q: How do I format JSON online?**
A: Paste your raw or minified JSON into the input field above and click "Format JSON." The tool instantly parses your data, validates the syntax, and displays a properly indented version with 2-space indentation. You can then copy the result to your clipboard with one click. Everything runs locally in your browser — no data is sent to any server.
**Q: How do I validate JSON?**
A: Paste your JSON into the input field and click "Format JSON." If the JSON contains syntax errors, the tool displays a detailed error message showing what went wrong and where. If the JSON is valid, it will be formatted and displayed in the output area. This tool validates against RFC 8259, the current JSON standard.
**Q: How do I minify JSON?**
A: Paste your JSON into the input field and click "Minify JSON." The tool removes all unnecessary whitespace, line breaks, and indentation to produce the most compact representation. Minified JSON is ideal for API responses, configuration files in production, and anywhere file size or bandwidth matters.
**Q: Is my JSON data safe when using this tool?**
A: Yes, completely. All processing happens locally in your browser using JavaScript's native JSON.parse() and JSON.stringify() — your data never leaves your device. There are no server uploads, no cookies, no analytics tracking on your input, and no data storage of any kind. This makes it safe to use with API keys, credentials, and proprietary data.
**Q: How do I fix "Unexpected token" errors in JSON?**
A: An "Unexpected token" error means the JSON parser found a character that doesn't belong at that position. The most common causes are: a missing comma between elements ({"name": "Alice" "age": 30}), a trailing comma after the last element ({"name": "Alice",}), or extra characters after the JSON ends. Paste your JSON into this tool to see the exact error location, then check the characters around that position.
**Q: Why does my JSON have a "trailing comma" error?**
A: JSON does not allow a comma after the last element in an object or array. This is one of the most common errors because JavaScript and many other languages permit trailing commas. For example, {"name": "Alice", "age": 30,} is invalid JSON — remove the comma after 30 to fix it. If you frequently copy data from JavaScript code, always check for trailing commas before using it as JSON.
**Q: Can I use single quotes in JSON?**
A: No. JSON requires double quotes for all strings and property keys. Single quotes are valid in JavaScript and Python, but they are not part of the JSON specification (RFC 8259). For example, {'name': 'Alice'} is invalid — it must be {"name": "Alice"}. If you have data with single quotes, this tool will report a syntax error and show you the exact position to fix.
**Q: Can I add comments to JSON?**
A: No, standard JSON does not support comments of any kind — no //, /* */, or # syntax. This was an intentional design decision to keep JSON simple and parseable. If you need comments in configuration files, consider JSONC (JSON with Comments, used by VS Code and TypeScript), JSON5, or YAML. To use commented files as standard JSON, strip the comments before parsing.
**Q: Why is my JSON not parsing correctly?**
A: The most common reasons JSON fails to parse are: (1) trailing commas after the last element, (2) single quotes instead of double quotes, (3) unquoted property keys, (4) comments in the data, (5) missing or extra brackets/braces, (6) unescaped special characters like backslashes or newlines inside strings. Paste your JSON into this tool — it will pinpoint the exact error type and location so you can fix it quickly.
**Q: What is the difference between JSON and YAML?**
A: Both JSON and YAML are data serialization formats, but they differ in design philosophy. JSON uses braces, brackets, and double quotes with a strict syntax — making it ideal for machine parsing and APIs. YAML uses indentation and minimal punctuation — making it more human-readable and popular for configuration files (Docker Compose, Kubernetes, GitHub Actions). JSON is a subset of YAML, so any valid JSON is also valid YAML, but not vice versa.
**Q: What is JSON Schema?**
A: JSON Schema is a separate standard (not part of JSON itself) that defines the expected structure, types, and constraints of JSON data. For example, you can specify that a field must be a string, a number must be between 1 and 100, or an array must contain at least one element. JSON Schema is widely used for API request/response validation, form generation, and documentation. This tool validates JSON syntax, not JSON Schema — for schema validation, use a dedicated JSON Schema validator. For end-to-end validation patterns in Node, Python, and the browser, see our
complete JSON Schema validation guide .
**Q: What is the difference between JSON and JSON5?**
A: JSON5 is an extension of JSON that adds features developers frequently request: single and double quotes, trailing commas, comments (// and /* */), unquoted keys, multiline strings, and hexadecimal numbers. JSON5 is often used in configuration files where human editing is common. Standard JSON parsers cannot read JSON5 — you need a JSON5 parser. This tool works with standard JSON (RFC 8259) only.
**Q: What is the maximum size of a JSON file?**
A: The JSON specification itself has no file size limit. Practical limits depend on the parser and environment: browsers typically handle JSON up to 500 MB–1 GB before running into memory issues, while server-side parsers (Node.js, Python, Java) can handle larger files with streaming parsers. This online tool efficiently handles JSON up to about 10 MB. For very large JSON files, consider using command-line tools like jq or streaming parsers.
**Q: I have a large API response that's completely minified — what's the fastest way to make it readable for debugging?**
A: Paste the minified JSON into this tool and click Format JSON. It will instantly parse and pretty-print the data with 2-space indentation, making nested objects and arrays immediately visible. For very large responses (5-10 MB), this browser-based tool is often faster than VS Code or command-line jq because it uses the browser's native JSON.parse() with zero startup overhead. You can also use the keyboard shortcut Ctrl+V to paste and the result appears instantly. For programmatic formatting, use JSON.stringify(data, null, 2) in JavaScript or python -m json.tool from the command line.
**Q: I keep getting JSON parse errors when copying data from my JavaScript code — what am I doing wrong?**
A: The most common cause is that JavaScript object literals are not valid JSON. Three key differences trip people up: (1) JavaScript allows single quotes ('name') but JSON requires double quotes ("name"); (2) JavaScript allows trailing commas ({"a": 1,}) but JSON does not; (3) JavaScript allows unquoted keys ({name: "Alice"}) but JSON requires quoted keys ({"name": "Alice"}). Additionally, JavaScript comments (// or /* */) are not valid in JSON. Paste your data into this tool — it will pinpoint the exact error type and position so you can fix it quickly. If you frequently need to convert JS objects to JSON, consider using JSON5 format as an intermediate step.
---
### JSON Schema Validator
URL: https://go-tools.org/tools/json-schema-validator
Validate JSON against any JSON Schema instantly in your browser. Supports Draft 2020-12, 2019-09, and Draft-07 with path-precise error messages. 100% private — no upload, no account, free.
#### What is a JSON Schema Validator?
A JSON Schema validator is a program that takes two JSON documents — a data document and a schema document — and reports whether the data conforms to the schema's contract. The schema declares field types, required keys, value ranges, allowed enum values, regex patterns, and structural rules using a fixed vocabulary (type, properties, required, items, enum, oneOf, allOf, $ref, format). The validator walks both documents in parallel and emits zero or more errors, each pinned to a JSON Pointer path inside the data.
Validation runs at runtime, at the boundary between untrusted input and your code. TypeScript types vanish at compile time and cannot help with JSON arriving from a webhook, a third-party API, or a user paste — that gap is exactly what JSON Schema fills. Pair it with TypeScript (or Pydantic in Python) and you get compile-time guarantees inside your codebase plus runtime guarantees at the boundary.
Draft 2020-12 is the current spec and what you should pick for new projects in 2026. Earlier drafts (2019-09, Draft-07, Draft-06, Draft-04) survive in legacy codebases — Draft-07 is still common in Helm charts, VS Code settings, and older Ajv configs. OpenAPI 3.1 uses Draft 2020-12 natively; OpenAPI 3.0 uses a Draft 4 subset.
This tool runs entirely in your browser. Your JSON, your schema, and the validation output never leave your machine — safe for proprietary API contracts and sensitive payloads. Internal $ref pointers resolve automatically; external HTTP refs are intentionally disabled to preserve privacy.
Working with adjacent JSON tools? Format the JSON with
JSON Formatter before pasting; compare two JSON documents with
JSON Diff ; convert with
JSON to YAML and
YAML to JSON . For end-to-end validation in Node, Python, and the browser, see our
JSON Schema validation guide .
```
// A 5-line schema that catches three real bugs
const schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": { "type": "integer", "minimum": 1 },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 150 }
},
"required": ["id", "email"],
"additionalProperties": false
};
// Three bugs the schema catches:
const bad = { "id": "42", "age": 200 };
// /id → type: expected integer, got string
// /email → required: missing
// /age → maximum: 200 > 150
// In Node: new Ajv().compile(schema)(bad) // false; ajv.errors has the paths
// In Python: jsonschema.validate(bad, schema)
// In the browser: this tool — same errors, same paths, no install
```
#### FAQ
**Q: What is JSON Schema validation?**
A: JSON Schema validation checks whether a JSON document matches a contract written in JSON Schema syntax. The schema declares field types, required keys, allowed values, and structural rules; the validator walks both documents in parallel and reports any path that violates the contract. It runs at the boundary between untrusted input (API request, webhook, config file, form payload) and your business logic — catching shape errors before they corrupt downstream code. See our
complete guide to JSON Schema validation for end-to-end Node, Python, and browser examples.
**Q: Which JSON Schema drafts does this validator support?**
A: Draft 2020-12 (default and recommended), Draft 2019-09, and Draft-07. The validator auto-detects the draft from the schema's $schema URI when present and falls back to your selection from the dropdown otherwise. For new projects in 2026 use Draft 2020-12 — it is what OpenAPI 3.1 uses natively and what Ajv defaults to. Draft-07 remains common in legacy codebases (older AJV setups, Helm charts, VS Code settings).
**Q: How do I validate JSON against a schema?**
A: Paste your JSON data into the left panel and your JSON Schema into the right panel. The validator runs instantly as you type — green check means valid, red list means errors. Each error includes a JSON Pointer path (for example /user/email), the failing keyword (type, required, pattern, minimum), and a human-readable message. Click any error to jump to the offending line. No upload, no signup.
**Q: What's the difference between JSON Schema validation and JSON syntax validation?**
A: JSON syntax validation only confirms the document parses — no extra commas, no missing braces.
JSON Formatter handles that. JSON Schema validation runs after parsing and checks whether the parsed structure matches the contract: required fields present, types correct, values within range. You typically run both — format first to confirm parseable, then validate against the schema.
**Q: Why is my schema rejecting JSON that looks correct?**
A: Five usual suspects: (1) additionalProperties: false — your data has a key the schema didn't declare, often a typo or a new field; (2) type: "integer" vs "number" — JSON Schema treats 1.0 as a number, not an integer; (3) format keywords (email, uri, uuid) reject malformed strings even when they look fine; (4) required at the wrong nesting level — required must appear next to properties, not inside one; (5) the JSON has a string "42" where the schema expects integer 42. The error path will pinpoint which one.
**Q: Does this support $ref and remote schema references?**
A: Internal $ref pointers (#/$defs/foo, #/properties/bar) work out of the box. Remote $ref to external URLs is intentionally disabled — fetching external schemas would leak your validation activity to third parties and break the privacy model. To validate against a multi-file schema, inline the referenced definitions into a single document using $defs, or run validation in your own CI with Ajv where remote refs are appropriate.
**Q: What does additionalProperties: false do?**
A: additionalProperties: false rejects any key that isn't declared in properties. It's the single most useful keyword for tightening contracts — without it, schemas are open by default and silently accept misspelled or malicious fields. Always set additionalProperties: false on input contracts (request bodies, config files, queue messages). Leave it true (or omit it) only when the schema is a partial description of a larger document.
**Q: How do I validate JSON against a schema in Node.js or Python?**
A: Node: install Ajv (npm i ajv ajv-formats), call new Ajv().compile(schema), then validate(data). Python: install jsonschema (pip install jsonschema), call jsonschema.validate(data, schema). For TypeScript, generate types from the schema using json-schema-to-typescript so compile-time and runtime stay in sync. Our
JSON Schema validation guide has copy-paste recipes for Node, Python, and browser.
**Q: What's the difference between oneOf, anyOf, and allOf?**
A: allOf — must match every subschema (intersection, used for composition). anyOf — must match at least one (union, fast-fail). oneOf — must match exactly one (discriminated union, slower but stricter). Use oneOf for discriminated unions like webhook events tagged by type; use anyOf for permissive unions; use allOf to extend a base schema with extra constraints. oneOf is the slowest because it tests every branch — prefer anyOf when exactly-one isn't required.
**Q: Does it support OpenAPI schemas?**
A: OpenAPI 3.1 uses Draft 2020-12 natively, so any OpenAPI 3.1 schema component pastes in directly. OpenAPI 3.0 uses a Draft 4 subset that's mostly compatible — you may hit edge cases around nullable: true (3.0 syntax) which Draft 2020-12 expresses as type: ["string", "null"]. For full OpenAPI document validation (paths, operations, security), use a dedicated OpenAPI linter like Spectral; this tool focuses on the schema portion.
**Q: Why does the validator say my JSON Schema is itself invalid?**
A: JSON Schema is a JSON document that must be valid JSON before it can be a valid schema. Common causes: trailing comma in a properties object, single quotes instead of double, $schema set to a non-existent draft URL, or required listed as a string instead of an array. Format the schema in
JSON Formatter first to surface syntax issues, then paste it back here for semantic validation.
**Q: Does the tool send my JSON or schema to a server?**
A: No. All parsing and validation runs locally in your browser. Your JSON, your schema, and the validation results never leave your machine — no upload, no localStorage of inputs, no analytics on what you paste. Safe for proprietary API contracts, internal config files, and sensitive payloads. Only your draft selection persists in localStorage so it survives a refresh; clear browser data to wipe it.
**Q: Can I validate JSON Lines (NDJSON) or multiple documents?**
A: This tool validates one document per run. For JSON Lines, validate each line individually or use Ajv in Node with a schema and a stream parser like JSONStream. For batch validation of large datasets, prefer the command-line — ajv-cli or check-jsonschema (Python) handle thousands of files per second with a single schema compile.
---
### JSON to CSV Converter
URL: https://go-tools.org/tools/json-to-csv
Convert JSON to CSV in your browser. RFC 4180, Excel-EU, TSV, Pipe presets. Flatten nested or stringify. 100% private, no upload.
#### What is CSV and Why Convert from JSON?
CSV (Comma-Separated Values) is the oldest and most widely supported tabular data format in computing — every spreadsheet app, every database, every analytics tool, and most programming languages have first-class CSV support. JSON, by contrast, is the universal format for API responses, configuration, and structured data exchange. Converting between them is one of the most common chores in data engineering: you receive JSON from an API or a NoSQL database, and you need a CSV to load into Excel for analysis, into a Postgres table via COPY, or into a BigQuery / Snowflake warehouse. This tool is built for that conversion path and handles four scenarios that most online converters botch.
This tool has four important differentiators compared to typical online converters:
**1. RFC 4180 State-Machine Parser.** CSV looks simple but the quoting rules are subtle: a field wrapped in double quotes can contain commas, embedded newlines, and escaped double quotes (doubled, like ""). Naive split-by-comma parsers break on real-world data — addresses with commas, multiline text fields, and quoted values containing quotes. This tool implements a proper state-machine parser following RFC 4180 (the IETF spec for CSV), correctly handling quoted fields, embedded delimiters, embedded line endings, and escaped quotes in every direction. The output is round-trippable through Python's csv module, PostgreSQL COPY, AWS S3 SELECT, and any compliant parser.
**2. Flatten One-Way / Stringify Reversible.** Nested JSON is fundamentally incompatible with CSV's flat tabular shape, and most converters silently corrupt data when they hit a nested object or array. This tool gives you an explicit choice: Flatten mode emits dotted keys (customer.address.city) and indexed keys (items.0.sku) for the cleanest spreadsheet layout — readable in Excel but lossy for round-trips. Stringify mode keeps arrays and objects as JSON inside a single cell — uglier but fully round-trippable: CSV → JSON → CSV produces identical data when paired with Infer types on the reverse. Choose based on your goal: analysis in Excel (Flatten) or pipeline round-trips (Stringify).
**3. Big-Integer Detection.** JavaScript's Number type uses IEEE 754 double precision and silently rounds integers above 2^53 - 1 (9007199254740991). This bites real-world JSON: Twitter snowflake IDs, Discord IDs, MongoDB Long fields, and Kubernetes resourceVersion are all 64-bit integers that exceed the safe range. Most browser-based JSON tools silently produce wrong numbers without warning. This tool detects big-integer values during parsing, shows a warning banner listing affected fields, and preserves the original digits as strings in the CSV output so Excel and Google Sheets won't truncate them to scientific notation.
**4. 100% Browser-Based Privacy.** Your JSON data — which often contains user PII, internal database exports, API keys embedded in payloads, and production secrets — never leaves your browser. No data is sent to any server, no logging, no analytics that capture input. You can verify this in your browser's Network tab. This is the only safe way to handle sensitive data in an online tool. See the reverse direction by clicking Swap or use our companion
JSON to YAML Converter when YAML is your target. Need to validate JSON before converting? Try our
JSON Formatter .
CSV's strengths are universality and simplicity: every tool reads it, parsers are tiny, and the file format is human-readable in any text editor. Its weaknesses are the lack of type information (everything is a string until you tell the parser otherwise), no native nested-structure support, and locale-specific quirks (Excel-EU semicolons, Windows CRLF vs Unix LF). JSON's strengths are exactly the opposite: precise types, native nesting, and a strict spec that parses identically everywhere. The right tool depends on the consumer: human reading a spreadsheet → CSV, machine consuming an API → JSON. This converter handles the bridge in both directions.
```
// Input JSON
[
{ "id": 1, "name": "Alice", "role": "admin" },
{ "id": 2, "name": "Bob", "role": "editor" }
]
// Output CSV (RFC 4180 preset: comma + CRLF + no BOM)
id,name,role
1,Alice,admin
2,Bob,editor
// Same input with Stringify mode + nested data
[
{ "id": 1, "tags": ["a", "b"] }
]
// Becomes
id,tags
1,"[""a"",""b""]"
```
#### FAQ
**Q: What does this tool do?**
A: It converts JSON to CSV directly in your browser, with bidirectional support: click Swap direction to convert CSV back to JSON in the same panel. Paste JSON in the input area and the tool produces CSV output instantly — no upload, no signup, nothing leaves your machine. The output respects your chosen preset (RFC 4180, Excel, TSV, or Pipe) so you can paste straight into Excel, Google Sheets, a database COPY command, or any data pipeline. The tool handles flat arrays of objects, nested structures (via Flatten or Stringify mode), NDJSON line-delimited input, and big-integer values that would otherwise lose precision in spreadsheet apps.
**Q: Is my data uploaded anywhere?**
A: No. All conversion runs 100% client-side in your browser using JavaScript. Your JSON data is never transmitted, never stored on any server, never logged, and never analyzed. This makes the tool safe for API responses containing PII, internal database exports, MongoDB dumps, and any sensitive data. You can verify this in your browser's Network tab — pasting JSON triggers zero network requests. The tool uses no cookies for input data and no third-party analytics that would capture what you paste.
**Q: What's the difference between Flatten and Stringify mode?**
A: Flatten mode emits dotted keys for nested objects and indexed keys for nested arrays (customer.address.city, items.0.sku) so each leaf value lives in its own column. This is the most readable layout for analysis in Excel or BigQuery, but it is lossy for round-trips because the dotted-key structure cannot be perfectly reconstructed. Stringify mode keeps arrays and objects as JSON inside a single cell ({"name":"Alice","city":"Seattle"}) — uglier in a spreadsheet, but fully round-trippable: CSV → JSON → CSV produces identical data. Choose Flatten for analysis, Stringify for round-trip safety. Pick before you convert; switching mid-session re-runs the conversion on the current input.
**Q: How does it handle big integers like Twitter IDs or Snowflake keys?**
A: Big integers (above 2^53 - 1, or 9007199254740991) are detected during JSON parsing and a warning banner appears below the output. The tool preserves the original digits as strings in the CSV so Excel and Google Sheets won't truncate them to scientific notation. This matters because JavaScript's IEEE 754 double-precision float silently rounds integers above 2^53 — for example, 9007199254740993 becomes 9007199254740992. To preserve precision when generating the JSON upstream, store these IDs as strings ("id": "9007199254740993"). The tool will keep them as strings in the CSV without any precision loss.
**Q: Why is Excel showing my CSV in one column?**
A: European Excel locales (Germany, France, Spain, Italy, etc.) expect a semicolon delimiter because the comma is reserved for decimal separators. When you open a comma-delimited CSV in Excel-EU, every row collapses into column A. Use the Excel preset on this tool — it switches the delimiter to ;, the line ending to CRLF, and adds a UTF-8 BOM so Excel correctly detects encoding and column boundaries. If you are sharing CSVs across regions, the safer option is TSV (Tab delimiter) which Excel handles consistently in every locale.
**Q: Does this support NDJSON or JSON Lines?**
A: Yes. NDJSON (.ndjson) and JSONL (.jsonl) are line-delimited formats where each line is one valid JSON value. Paste the file contents directly into the input area — the tool auto-detects the format by looking for multiple top-level JSON values separated by newlines, and treats each line as a row in the output CSV. This is the natural shape for streaming logs, API event exports, and many data lake pipelines. NDJSON does not require a wrapping array, so you do not need to manually merge lines into one JSON document.
**Q: What is RFC 4180?**
A: RFC 4180 is the IETF specification that codified the de facto CSV format in 2005. It defines the rules for delimiters (typically comma), line endings (CRLF), the optional header row, and most importantly the quoting rules: fields containing the delimiter, double quote, CR, or LF must be wrapped in double quotes, and embedded double quotes are escaped by doubling them (""). The RFC 4180 preset on this tool produces output strictly compliant with the spec: comma delimiter, CRLF line endings, no BOM, double-quote auto-escaping. This is the safest choice for interoperability with parsers in Python (csv module), PostgreSQL COPY, AWS S3 SELECT, and most data pipelines.
**Q: Why are some cells wrapped in quotes and others not?**
A: The default Quote mode is Auto, which follows RFC 4180: a cell is wrapped in double quotes only when it contains the delimiter, a double quote, a carriage return, or a newline. This produces the cleanest, most human-readable CSV — values like Alice or 42 stay unquoted, while values like Smith, Jr. or Line 1\nLine 2 get wrapped. Switch to Always quote mode to wrap every cell, even simple ones — useful when downstream tools have buggy CSV parsers that misinterpret unquoted values, or when your team's pipeline expects every field to be quoted for consistency.
**Q: Can I round-trip CSV → JSON → CSV without data loss?**
A: Yes, when the input is flat (no nested objects or arrays). For nested data, you must use Stringify mode — it keeps arrays and objects as JSON inside a single cell, which round-trips losslessly back to the original structure when you reverse with Swap direction and Infer types. Flatten mode is one-way: it emits dotted keys (customer.address.city) that cannot be perfectly reconstructed because the parser cannot distinguish a dotted key from a nested path. The tool detects nested structures and shows a Schema notes warning when round-trip safety is at risk so you can switch modes before exporting.
**Q: How do I get a TSV file?**
A: Click the TSV preset chip. This switches the delimiter to Tab, the line ending to LF, and disables the BOM — the standard format for tab-separated values used by Unix tools (cut, awk), data warehouses (BigQuery, Snowflake), and most Excel locales without ambiguity. TSV is generally safer than comma-CSV for cross-locale sharing because Tab is unlikely to appear inside text fields, eliminating most quoting edge cases. Save the output with a .tsv or .tab extension and most tools will recognize it automatically.
**Q: What happens with very large input?**
A: Above 100,000 characters or 2,000 rows, live conversion automatically switches to manual mode: a Convert button appears in an info banner and conversion only runs when you click it. This prevents the browser's main thread from blocking on every keystroke during heavy serialization. For output above 5 MB or 50,000 rows, the tool truncates the on-screen preview to the first 500 rows and shows a Showing the first 500 of N rows banner — but the Download button still produces the full file with every row included. Hard upper limit is 10 MB of input; above that the tool shows an error and asks you to reduce the input.
**Q: What encodings are supported?**
A: Input and output are both UTF-8. UTF-8 covers every modern character set including emoji, CJK ideographs, Arabic, Hebrew, and combining marks. The only encoding nuance is the optional UTF-8 BOM (Byte Order Mark): Excel on Windows traditionally needs the BOM to detect UTF-8 correctly, otherwise it falls back to the system locale and mangles non-ASCII characters. Toggle BOM on (or use the Excel preset, which enables BOM by default) when you plan to open the CSV in Excel. Leave BOM off for everything else — most modern parsers (PostgreSQL, Pandas, jq, Python csv) will choke or include the BOM as a stray character at the start of the first cell.
---
### JSON to .env Converter
URL: https://go-tools.org/tools/json-to-env
Paste a JSON object, get a .env file instantly. Generate dotenv from config locally — your keys and secrets never leave your browser. 100% private, no upload.
#### What is a .env File and Why Generate One from JSON?
A .env file (dotenv file) is a plain-text list of KEY=VALUE pairs that holds environment configuration and secrets outside your source code. It is the de facto standard for Node.js, Vite, Next.js, Python, Ruby and Docker Compose — the dotenv library loads the file and injects each pair into the process environment. Because it commonly stores database passwords, API keys and access tokens, a .env file is treated as sensitive and kept out of version control.
Generating a .env file from JSON is the reverse of the common parse-config task: you already have configuration as a JSON object — from an API response, a config export, a secrets-manager dump, or a script that builds settings programmatically — and you need a .env file to drop into a project or hand to a container. This converter walks the top-level keys of your JSON object and writes one correctly quoted KEY=VALUE line per property.
This tool is built around a few deliberate decisions:
**1. Round-trip-safe quoting.** Numbers and booleans are written bare, null becomes an empty value, and any string that contains a space, newline, # or quote is automatically double-quoted and escaped. The result parses back cleanly through dotenv and through the companion
.env to JSON Converter , so a value never changes meaning on the round trip.
**2. Honest handling of nesting.** A .env file is flat by definition. Rather than silently dropping nested data, the tool serializes each nested object or array to a compact JSON string and warns you which keys were flattened, so you can decide whether .env is really the right target.
**3. Optional key normalization.** Keys are kept verbatim by default to avoid losing information. Turn on Normalize keys to convert camelCase or kebab-case into the UPPER_SNAKE_CASE convention environment variables use, with a warning for any key that still cannot form a valid name.
**4. 100% browser-based privacy.** The JSON you paste — usually the very credentials you are about to write into a .env — never leaves the browser. No upload, no server round-trip, no logging; verify zero network requests in the DevTools Network tab.
Before converting, you can validate or pretty-print the JSON with the
JSON Formatter , or unescape a JSON string with
JSON Escape . If your configuration is better expressed with structure,
JSON to YAML preserves nesting that a flat .env cannot.
```
// Generate .env lines from a JSON object in Node.js
const config = {
DATABASE_URL: 'postgres://user:pass@localhost:5432/mydb',
PORT: 8080,
DEBUG: true,
NOTE: 'value with spaces',
};
const needsQuotes = (s) => /[\s#"'\n]/.test(s);
const env = Object.entries(config)
.map(([key, value]) => {
if (typeof value === 'string') {
return needsQuotes(value)
? `${key}=${JSON.stringify(value)}`
: `${key}=${value}`;
}
return `${key}=${value ?? ''}`; // null -> empty value
})
.join('\n');
console.log(env);
// DATABASE_URL=postgres://user:pass@localhost:5432/mydb
// PORT=8080
// DEBUG=true
// NOTE="value with spaces"
```
#### FAQ
**Q: How do I convert JSON to a .env file online?**
A: Paste a JSON object into the input field above. The tool generates a .env file instantly in your browser — no button click needed. Each top-level property becomes a KEY=VALUE line. You can optionally normalize keys to UPPER_SNAKE_CASE or add an export prefix from the Options panel, then click Copy to grab the result or Download to save it as a .env file. Everything runs locally, so your secrets never leave your device.
**Q: What kind of JSON does this accept?**
A: The input must be a JSON object (a set of key/value pairs at the top level), because a .env file is fundamentally a flat list of variables. A top-level array or a bare scalar like a string or number cannot map to environment variables, so the tool reports an error asking for an object. Invalid JSON also produces an error with best-effort line and column numbers so you can locate the problem quickly.
**Q: How are strings, numbers, booleans and null written?**
A: Numbers and booleans are written without quotes (PORT=8080, DEBUG=true). A null value becomes an empty assignment (KEY=), which dotenv loads as an empty string. Plain strings are written as-is, but a string containing spaces, a newline, a #, or a quote character is automatically wrapped in double quotes and escaped so it parses back correctly. This means the output round-trips cleanly through the dotenv parser and through our companion
.env to JSON Converter .
**Q: What happens to nested objects and arrays?**
A: .env files cannot represent nesting — every variable is a flat string. When a value is a nested object or array, the tool serializes it to a compact JSON string with JSON.stringify, wraps it in double quotes, and escapes it. A non-blocking warning lists exactly which keys were flattened this way, so you always know the structure was collapsed. If your data is deeply nested, a format like
JSON to YAML preserves the hierarchy far better than .env can.
**Q: What does the Normalize keys option do?**
A: By default the original JSON keys are kept exactly as written, so no data is lost — and in that mode any key that is not already a valid environment variable name (most shells and loaders only accept names matching [A-Za-z_][A-Za-z0-9_]*) is flagged with a warning so you can rename it. With Normalize keys enabled, keys are converted to UPPER_SNAKE_CASE — the conventional style for environment variables (databaseUrl becomes DATABASE_URL, enable-signup becomes ENABLE_SIGNUP) — which resolves most invalid names automatically.
**Q: Is my JSON data sent to a server?**
A: No. All conversion happens entirely in your browser with JavaScript. The JSON you paste — which often holds API keys, database credentials and tokens you are about to write into a .env file — is never transmitted, never stored on any server, and never logged. You can confirm this by opening your browser's Network tab and watching that pasting triggers zero requests. That is what makes it safe to generate a real production .env, not just a sample.
---
### JSON to Python Class Converter
URL: https://go-tools.org/tools/json-to-python
Paste JSON, get Python classes instantly — dataclass, Pydantic v2, or TypedDict. Correct Optional typing, camelCase aliases, nested classes. 100% in your browser, free.
#### What is JSON to Python conversion?
JSON to Python conversion turns a JSON sample into ready-to-use Python classes — a standard-library dataclass, a Pydantic v2 model, or a TypedDict — so you never hand-write field definitions for an API response or config file. This Python class generator infers correct types (int, float, str, bool, Optional), turns nested objects into named classes, and adds Pydantic aliases for camelCase keys, all 100% in your browser.
#### FAQ
**Q: How do I convert JSON to a Python class?**
A: Paste your JSON into the input box. The converter parses it instantly in your browser and generates Python on the right. Pick dataclass, Pydantic v2, or TypedDict with the toggle, then click Copy — no upload, no account, no waiting.
**Q: What is the difference between dataclass, Pydantic, and TypedDict output?**
A: dataclass gives you a standard-library @dataclass with no dependencies — great for plain data holders. Pydantic v2 emits BaseModel classes that validate and coerce data at runtime, ideal for parsing untrusted API responses. TypedDict describes the shape of a plain dict for static type checkers (mypy, Pyright) with zero runtime cost. Switch modes to compare the same JSON in each.
**Q: How do I generate a Pydantic model from JSON?**
A: Choose the Pydantic v2 tab. Fields are converted to snake_case with Field(alias="originalKey") so the model still reads camelCase JSON, and model_config = ConfigDict(populate_by_name=True) lets you construct it by field name too. Parse a payload with Root.model_validate(data) — Pydantic validates types and raises a clear error on bad input.
**Q: How does the dataclass output handle camelCase keys?**
A: A dataclass has no built-in alias, so to keep Root(**data) working, dataclass mode keeps the original key as the field name when it is a valid Python identifier (publicRepos stays publicRepos). If you want idiomatic snake_case with aliases, use the Pydantic v2 mode instead, which maps snake_case fields back to the exact JSON key.
**Q: How are optional and null fields typed?**
A: When a key appears in some array items but not others, its type is wrapped in Optional. A field that is only ever null becomes Optional[Any], because JSON null alone carries no type. Paste a representative sample with a filled-in value to get a more specific type than Any.
**Q: What Python type does each JSON value map to?**
A: Strings map to str, booleans to bool, whole numbers to int, and any number with a decimal point or exponent to float. Because Python integers are arbitrary precision, even huge IDs stay int — there is no 64-bit overflow. Empty or mixed-type arrays become List[Any], and objects become nested classes.
**Q: Does it handle nested objects and arrays of objects?**
A: Yes. Each nested object becomes its own named class, and identical shapes are deduplicated into a single class reused by every field. Arrays of objects are merged key by key so you get one element class, with keys missing from some items marked Optional. Child classes are always emitted before the classes that use them.
**Q: How are Python keywords and non-identifier keys handled?**
A: A JSON key that is a Python keyword (class, from, import) gets a trailing underscore (class_). Keys with hyphens, spaces, or a leading digit are sanitized to valid identifiers in dataclass and Pydantic modes. In TypedDict mode, any dict containing a non-identifier key is emitted with the functional TypedDict('Name', {...}) syntax so the exact key like "first-name" is preserved.
**Q: How do I use the generated dataclass to parse JSON?**
A: For a flat object, json.loads then Root(**data) works directly. For nested structures, dataclass does not recurse automatically — either build the child objects yourself, use a library like dacite or pydantic.dataclasses, or switch this tool to Pydantic v2 mode, where Root.model_validate(json.loads(text)) parses the whole tree in one call.
**Q: Is my JSON data private and safe?**
A: Yes. Conversion runs 100% in your browser with JavaScript. Your JSON — including tokens, IDs, or customer data — never leaves the page and is never sent to a server.
**Q: Is the tool free? Do I need an account?**
A: It is completely free with no sign-up, no limits, and no ads cluttering the workspace. It works offline once the page has loaded.
---
### JSON to Rust Struct Converter
URL: https://go-tools.org/tools/json-to-rust
Paste JSON, get idiomatic Rust serde structs instantly, 100% in your browser. Correct i64/u64/f64 typing, Option for nulls, #[serde(rename)] for camelCase. Free.
#### What is JSON to Rust conversion?
JSON to Rust conversion turns a JSON sample into ready-to-compile Rust structs with serde's #[derive(Serialize, Deserialize)] macros, so you never hand-write deserialization boilerplate for API responses or config files. This fast Rust struct generator infers correct number types, marks absent fields as Option, and adds #[serde(rename)] for non-snake_case keys — all 100% in your browser.
#### FAQ
**Q: How do I convert JSON to a Rust struct?**
A: Paste your JSON into the input box. The converter parses it instantly in your browser and generates Rust structs with serde derives on the right. Click Copy to grab the result — no upload, no account, no waiting.
**Q: Does it generate serde derives? Do I need serde and serde_json?**
A: Yes — output uses #[derive(Debug, Clone, Serialize, Deserialize)] by default. Add serde with the derive feature to your Cargo.toml. You only need serde_json as a dependency if the output contains serde_json::Value, which appears for empty or mixed arrays and null-only fields. Turn off the serde toggle to emit plain structs.
**Q: How do I use the generated struct to parse JSON?**
A: Add serde_json to your Cargo.toml, then deserialize in one line: let root: Root = serde_json::from_str(json)?;. The generated Deserialize derive does the rest — use serde_json::from_slice for a byte slice or from_reader for a file or HTTP body, and serde_json::to_string to serialize back.
**Q: How are optional and null fields handled?**
A: When a key appears in some array items but not others, it becomes an Option field. A field that is only ever null becomes an optional serde_json::Value. serde treats Option as optional automatically, so no #[serde(default)] attribute is added or required.
**Q: How does it handle camelCase keys and Rust keywords?**
A: Field names are converted to idiomatic snake_case, and a #[serde(rename)] attribute maps them back to the exact JSON key. Reserved keywords like type or match are emitted as type_ or match_ with a rename, which is more robust than raw identifiers because it also covers self, crate, and super.
**Q: Can it use #[serde(rename_all)] instead of per-field renames?**
A: The tool emits a per-field #[serde(rename)] because it always works — even when one payload mixes camelCase, snake_case, and irregular keys. If every field in a struct shares one convention, delete those attributes and put a single #[serde(rename_all = "camelCase")] on the struct instead; both deserialize identically.
**Q: What Rust number type does it use?**
A: Integers map to i64, or u64 when a value exceeds i64::MAX, and fall back to f64 beyond u64 — so large IDs still round-trip. Any number written with a decimal point or exponent (like 1.0 or 2e3) maps to f64, because serde would reject a float into an integer field.
**Q: How are dates and timestamps typed?**
A: JSON has no date type, so ISO strings like 2011-01-25 or RFC 3339 timestamps come out as String. For real date handling, change the field to a chrono type — DateTime in the Utc time zone, or NaiveDate — and enable chrono's serde feature. serde then parses RFC 3339 automatically.
**Q: How do I handle objects with dynamic or unknown keys?**
A: When keys vary — for example a map of IDs to values — replace the generated struct with a HashMap keyed by String. To keep a typed struct but still capture extra fields, add a #[serde(flatten)] field that is a HashMap. For fully dynamic values, serde_json::Value is the catch-all type.
**Q: Is my JSON data private and safe?**
A: Yes. Conversion runs 100% in your browser with JavaScript. Your JSON — including tokens, IDs, or customer data — never leaves the page and is never sent to a server.
**Q: Can I generate plain Rust structs without serde?**
A: Yes. Turn off the serde toggle to drop the use serde line, the Serialize and Deserialize derives, and all #[serde(rename)] attributes — leaving clean structs. You can also toggle Debug and Clone derives and pub visibility.
**Q: Is the tool free? Do I need an account?**
A: It is completely free with no sign-up, no limits, and no ads cluttering the workspace.
---
### JSON to TOML Converter
URL: https://go-tools.org/tools/json-to-toml
Paste JSON, get TOML instantly in your browser. Safe null handling, top-level table checks, Cargo.toml & pyproject.toml-ready output. 100% private, no upload.
#### What is TOML and Why Convert from JSON?
TOML (Tom's Obvious, Minimal Language) is a configuration file format built to be unambiguous and easy for humans to read and edit. It is the standard config format for the Rust ecosystem (Cargo.toml), modern Python packaging (pyproject.toml), and tools like Hugo, Netlify, Poetry, and Foundry. JSON is the universal machine format that tools and APIs produce. Converting JSON to TOML is common when you have structured data as JSON but need a human-editable TOML config as the destination — generating a Cargo.toml or pyproject.toml, or turning an application's JSON settings into a readable config file.
TOML is more structured than JSON, and this tool turns that strictness into guidance rather than cryptic failures:
**1. Top-level table enforcement.** Every TOML document is a table at the root — a bare array or scalar is not valid TOML. Instead of emitting broken output for { top-level array } input, this tool detects it and tells you exactly how to wrap your data under a key, so you always get valid TOML or a clear explanation.
**2. Safe, transparent null handling.** JSON has null; TOML does not. Most converters either crash or silently discard data. This tool is explicit: object keys with null values are dropped and the removed keys are listed in a warning, and a null inside an array — which has no valid TOML representation — produces a precise, path-pointing error. You are never surprised by missing data.
**3. Idiomatic TOML output.** Nested objects become [tables], deeply nested objects become dotted tables ([tool.ruff]), and arrays of objects become arrays of tables ([[section]]) — the shape real Cargo.toml and pyproject.toml files use. The conversion is powered by the zero-dependency, TOML 1.0.0-compliant smol-toml library.
**4. 100% browser-based privacy.** Your JSON — which may contain credentials, tokens, or internal service details — never leaves your browser. No upload, no server, no logging. Confirm it in your browser's Network tab.
Need the reverse? Use the
TOML to JSON Converter . Working with other config formats? Try the
JSON to YAML Converter and
YAML to JSON Converter , or clean up your JSON input first with the
JSON Formatter . Each format has its niche: JSON for machine interchange, TOML for human-edited application and tooling config, and YAML for deeply nested infrastructure manifests. This converter lets you move from JSON to TOML without writing any code.
```
// Convert JSON to TOML in Node.js using the smol-toml library
import { stringify } from 'smol-toml';
const data = JSON.parse(`{
"package": { "name": "my-app", "version": "1.0.0" },
"dependencies": { "serde": { "version": "1.0" } }
}`);
// The top-level value must be an object; null values on objects are dropped.
const toml = stringify(data);
console.log(toml);
// [package]
// name = "my-app"
// version = "1.0.0"
//
// [dependencies.serde]
// version = "1.0"
```
#### FAQ
**Q: How do I convert JSON to TOML online?**
A: Paste your JSON into the input field above. The tool parses it and produces TOML instantly in your browser — no button click needed. Once the TOML appears in the output area, click Copy to grab it to your clipboard or Download to save it as a .toml file. Everything runs locally, so your JSON never leaves your device. Two things to know before you start: your JSON's top level must be an object (not an array), and null values have no TOML equivalent — the tool explains both clearly if they come up.
**Q: What is TOML and why convert JSON to it?**
A: TOML (Tom's Obvious, Minimal Language) is a configuration format designed to be easy for humans to read and write, with unambiguous semantics. It is the config format for Rust's Cargo, Python's pyproject.toml, Hugo, Netlify, Poetry, and more. You convert JSON to TOML when a tool or API gives you JSON but the destination expects TOML — for example, generating a Cargo.toml or pyproject.toml from structured data, or turning an application's JSON settings into a human-editable TOML config file.
**Q: Why must the top level of my JSON be an object?**
A: TOML documents are always a table (a set of key-value pairs) at the root — the specification does not allow a bare array, string, or number at the top level. So a JSON array like [1, 2, 3] or a lone value like 42 cannot be converted directly. The fix is to wrap it under a key: { "items": [1, 2, 3] } converts cleanly to items = [1, 2, 3]. This tool detects a non-object top level and tells you exactly how to wrap it, instead of producing broken output.
**Q: What happens to null values when converting JSON to TOML?**
A: TOML has no null type, so null cannot be represented. This tool handles it safely and transparently in two ways. When a null appears as an object value, TOML simply omits that key — and the tool shows a warning listing exactly which keys were dropped, so you are never surprised by silent data loss. When a null appears inside an array, there is no valid TOML output at all (an array cannot hold a gap), so the tool reports an error pointing at the exact path of the offending null. Either way, you know precisely what happened and where.
**Q: How are nested objects and arrays converted to TOML?**
A: A nested JSON object becomes a TOML table: { "owner": { "name": "Tom" } } becomes [owner] with name = "Tom". Deeply nested objects become dotted tables like [tool.ruff]. An array of objects becomes an array of tables written with double brackets ([[servers]]), which is the idiomatic TOML way to express repeated sections. Arrays of scalars stay inline as arrays (ports = [8001, 8002]). The output follows the TOML 1.0.0 specification.
**Q: What happens to floating-point numbers like 1.0?**
A: TOML distinguishes integers from floats, and a float whose fractional part is zero (like 1.0) is written as the integer 1. So { "version": 1.0 } becomes version = 1. The tool shows a small warning when this coercion happens, because it changes the value's type on a round-trip. If you need the value to stay a float, that distinction cannot be preserved through TOML for whole numbers — consider whether an integer is actually what you want.
**Q: Is my JSON data sent to any server?**
A: No. All parsing and conversion happen entirely in your browser using JavaScript. Your JSON is never uploaded, never stored, and never logged. This makes the tool safe for configuration that contains API keys, database credentials, or internal service details. You can verify this by opening your browser's Network tab — pasting JSON triggers zero network requests.
**Q: Can I convert TOML back to JSON?**
A: Yes. Use the companion
TOML to JSON Converter for the reverse direction, or click the Swap direction button at the top of this tool to flip the input and output in place. TOML to JSON has fewer constraints — it accepts any valid TOML — so round-tripping JSON → TOML → JSON is reliable as long as your JSON had no nulls or top-level array to begin with.
**Q: How do I convert JSON to TOML on the command line?**
A: A popular option is the Go tool 'yj' (yj -jt reads JSON and writes TOML). In Python you can use the third-party 'tomli-w' package: import json, tomli_w; tomli_w.dump(json.load(open('config.json')), open('config.toml','wb')). In Node.js: import { stringify } from 'smol-toml'; const toml = stringify(JSON.parse(text)) — the same library this tool uses. For a quick one-off without installing anything, this browser tool is the fastest path.
**Q: How do I convert JSON to TOML in Python, Rust, or Node.js?**
A: In Python: import json, tomli_w; tomli_w.dump(json.load(open('config.json')), open('config.toml','wb')). In Rust: use serde_json and toml — let value: serde_json::Value = serde_json::from_str(&text)?; let toml = toml::to_string_pretty(&value)?. In Node.js: import { stringify } from 'smol-toml'; const toml = stringify(JSON.parse(text)) — this is exactly the approach used by this tool. Remember that the top-level value must be an object in all of these.
**Q: Does the converter preserve key order?**
A: Keys within a table are preserved in their original order, but TOML structure requires that all plain key-value pairs of a table come before any child table headers. So top-level scalars are emitted first, then tables and arrays of tables — the converter reorders only where the TOML grammar demands it. The data is identical; only the textual ordering of table sections shifts to produce valid TOML.
**Q: Is there a file size limit for JSON input?**
A: There is no hard limit, but inputs over 200KB switch from live conversion to manual mode: a Convert button appears and conversion runs only when you click it, keeping the browser responsive. Typical configuration payloads convert in well under 50 milliseconds.
---
### JSON to TypeScript Converter
URL: https://go-tools.org/tools/json-to-typescript
Paste JSON, get TypeScript interfaces instantly. 100% in your browser — data never leaves the page. interface or type, nested objects, arrays, optional fields. Free, no sign-up.
#### What is JSON to TypeScript conversion?
JSON to TypeScript conversion reads a JSON value and generates matching TypeScript interface or type definitions — eliminating hand-written boilerplate for API responses and config files. Paste a payload and get production-ready types in seconds, fully typed for nested objects, arrays, and optional fields.
#### FAQ
**Q: How do I convert JSON to a TypeScript interface?**
A: Paste your JSON into the input box. The converter reads it instantly in your browser and generates a TypeScript interface on the right. Click Copy to grab the result — no upload, no account.
**Q: Should I use `type` or `interface` for JSON data?**
A: Both work. `interface` is conventional for object shapes and gives slightly better editor errors; `type` is handy for unions and intersections. Use the Output toggle to switch between them and keep whichever your codebase prefers.
**Q: How are nested objects and arrays handled?**
A: Nested objects become separate, named interfaces (e.g. an `address` field yields an `Address` interface). Arrays of objects are merged into one element interface; primitive arrays become typed arrays like `string[]`.
**Q: How are optional and null fields handled?**
A: When a key is present in some array items but not others, it is marked optional. Choose `?:` (optional) or `| null` (explicit nullable) with the Optional fields toggle. Literal null values are typed as `null`.
**Q: How do I generate TypeScript types from JSON automatically in VSCode?**
A: You can install an extension, but you don't have to. This tool runs entirely in your browser — paste, copy, done — with no plugin to install, configure, or keep updated.
**Q: Is my JSON data private and safe?**
A: Yes. Conversion happens 100% in your browser using JavaScript. Your JSON — including any tokens, IDs, or customer data — never leaves the page and is never sent to a server.
**Q: Is the tool free? Do I need an account?**
A: It is completely free with no sign-up, no limits, and no ads cluttering the workspace.
**Q: Can it detect dates or enums?**
A: Date strings are kept as `string` (safer than guessing). String values are typed as `string` rather than literal unions, so the output stays stable as your data changes.
---
### JSON to XML Converter
URL: https://go-tools.org/tools/json-to-xml
Paste JSON, get XML instantly. Converts objects, arrays, and @_ attributes in-browser — nothing uploaded. Free, private, no signup required.
#### What is JSON-to-XML Conversion and How Does It Work?
JSON (JavaScript Object Notation) and XML (Extensible Markup Language) are both structured data formats, but they have fundamentally different models: JSON is a tree of objects, arrays, strings, numbers, booleans, and null values with no concept of attributes or document root constraints; XML is a tree of elements that may carry attributes and text content, and the document must have exactly one root element. Converting from JSON to XML requires a set of conventions to bridge this mismatch.
This tool uses the most widely adopted convention — the same one used by fast-xml-parser (Node.js), xmltodict (Python), and JAXB (Java) — applied in reverse:
**1. Root element normalization.** The single most important difference between JSON and XML is the root constraint. JSON has no root concept; XML requires exactly one. The converter handles four cases automatically. A single-key object uses that key as the XML root: { "config": {...} } →
... . A multi-key object wraps in
: { "a": 1, "b": 2 } → 1 2 . A top-level array wraps as - ...
. A primitive value wraps as value .
**2. @_ prefix → XML attributes.** JSON keys prefixed with @_ become XML attributes on the enclosing element. { "element": { "@_id": "42", "@_class": "primary" } } produces . This prefix is the canonical convention — no valid XML element name starts with @, so there is never a collision with child element names.
**3. #text → element text content.** When an element needs both attributes and text content, the text is stored under the #text key: { "price": { "@_currency": "USD", "#text": "29.99" } } → 29.99 . Elements with only text content (no @_ keys) convert to plain text elements without this indirection.
**4. Arrays → repeated same-named sibling elements.** XML allows multiple child elements with the same name; JSON uses arrays for ordered lists. A JSON array under a key produces repeated child elements that reuse the key name: { "items": ["a", "b"] } produces a b (the two elements are siblings under the parent). When the entire JSON input is a top-level array, a wrapper is added and each element becomes an - child —
- is a fixed fallback name used only in that case.
**5. Symmetric with XML-to-JSON.** The @_ and #text conventions used here are exactly the same conventions used by the companion
XML to JSON Converter . This means a JSON → XML → JSON round-trip preserves attributes, text content, and element structure — as long as the input JSON follows the @_/#text conventions.
**When to convert JSON to XML?** The most common scenarios are: (1) sending data to a legacy SOAP or XML-based web service that requires an XML request body; (2) generating XML configuration files (Spring, Maven, Ant, Android resources) from JSON data; (3) producing sitemap.xml or RSS feed XML from JSON content data; (4) interoperating with enterprise systems (ERP, CRM, EDI) that consume XML; (5) generating SVG or other XML-based graphics formats programmatically from JSON data. For formatting and validating the resulting XML, use the XML Formatter .
```
// Convert JSON to XML in Node.js using fast-xml-parser
import { XMLBuilder } from 'fast-xml-parser';
const data = {
catalog: {
product: {
'@_id': 'P01',
'@_category': 'electronics',
name: 'Wireless Headphones',
price: {
'@_currency': 'USD',
'#text': '79.99'
}
}
}
};
const builder = new XMLBuilder({
attributeNamePrefix: '@_', // @_ keys become XML attributes
textNodeName: '#text', // #text key becomes element text content
ignoreAttributes: false, // process @_ attribute keys
format: true, // pretty-print with indentation
indentBy: ' ', // 2-space indent
});
const xml = builder.build(data);
console.log(xml);
//
//
// Wireless Headphones
// 79.99
//
//
```
#### FAQ
**Q: Is my JSON data sent to a server when I use this tool?**
A: No. All conversion happens entirely inside your browser using JavaScript. Your JSON is never transmitted over the network, never stored on any server, and never logged or analyzed. This makes the tool safe to use with JSON payloads containing API credentials, database configuration, internal service data, or any other sensitive content. You can verify this by opening your browser's Network tab — you will see zero requests triggered by pasting or converting JSON.
**Q: How does the tool decide what the XML root element is?**
A: XML requires exactly one root element; JSON has no such constraint. The converter applies these rules: (1) A single-key object uses that key as the root element name — { "user": { ... } } becomes ... . (2) A multi-key object (two or more keys at the top level) is wrapped in a element so all keys become children of a single root. (3) A top-level array is wrapped as - ...
, with each array element becoming an - child. (4) A primitive value (string, number, boolean, null) at the top level becomes
value . These rules guarantee the output is always well-formed XML with exactly one root.
**Q: Why does a multi-key JSON object get wrapped in ?**
A: XML is a document format with a strict single-root requirement — a valid XML document must have exactly one top-level element. JSON objects can have any number of top-level keys, so when your JSON has multiple top-level keys (such as { "status": 200, "data": {...}, "meta": {...} }), there is no single key to use as the root. Wrapping in is the safest and most predictable convention. If you want a different root element name, reshape your JSON to a single-key object before converting — e.g. { "response": { "status": 200, "data": {...} } }.
**Q: How does a top-level JSON array convert to XML?**
A: A top-level array is wrapped as - ...
- ...
. Each array element becomes an - child — "item" is a fixed literal name used only for top-level arrays. This is distinct from arrays nested under an object key: if you write { "products": [...] }, each array element becomes a
child (reusing the key name), not . If you want custom tag names for a top-level array, wrap it in a named object first: { "products": [...] } gives you repeated elements.
**Q: How do I convert JSON keys to XML attributes?**
A: Prefix the key with @_ and the converter will emit it as an XML attribute instead of a child element. For example, { "tag": { "@_id": "42", "@_lang": "en", "#text": "Hello" } } produces Hello . The @_-prefix convention is the same one used by fast-xml-parser (Node.js) and xmltodict (Python), making the output round-trip consistently with those libraries. This is also the convention used by the companion XML to JSON Converter .
**Q: What is the #text key used for?**
A: When an element needs both XML attributes and text content, you cannot simply map the text to a child element — it must be the element's own text node. The #text key in your JSON becomes that text content. Example: { "price": { "@_currency": "USD", "#text": "29.99" } } produces 29.99 . If an object has only a #text key and no @_ keys, it still produces a plain text element: { "note": { "#text": "hello" } } becomes hello .
**Q: Does indentation affect the XML structure?**
A: No. Indentation is purely cosmetic — it changes how the XML is formatted for human readability but does not affect the element structure, attribute values, or text content. Choose 2 spaces for compact output or 4 spaces for more readable output. Both produce semantically identical XML. Most XML parsers treat whitespace-only text nodes between elements as ignorable whitespace, so indented and minified XML are equivalent for parsing purposes.
**Q: How does a JSON array nested inside an object convert to XML?**
A: A JSON array value under a key produces repeated same-named child elements, reusing the key name for every element. For example, { "items": [1, 2, 3] } produces three siblings — not - . Similarly, { "products": [{"name":"A"},{"name":"B"}] } produces two
elements, each containing a child. The key name is used as-is for every array element; no singularization occurs. The only place the literal name - appears is when the entire JSON input is a top-level array (see above), where
- is a fixed fallback wrapper name.
**Q: How do I convert XML back to JSON?**
A: Use the companion
XML to JSON Converter . It applies the same @_ and #text conventions in reverse: XML attributes become @_-prefixed JSON keys, element text content paired with attributes becomes a #text key, and repeated same-named sibling elements become a JSON array. The two tools are symmetric for round-trip use cases.
**Q: Can I validate or format the XML output?**
A: Yes — paste the XML output into the XML Formatter to validate well-formedness, adjust indentation, or minify. The XML Formatter is the right tool for inspecting and polishing the XML once this converter has produced it.
**Q: Is there a file size limit for JSON input?**
A: There is no hard limit, but inputs larger than 200KB automatically switch from live conversion to manual mode. In manual mode a Convert button appears and conversion runs only when you click it — this keeps the browser responsive during heavy serialization. For very large JSON files (multi-megabyte), consider command-line tools for better performance: node -e "const {XMLBuilder}=require('fast-xml-parser');console.log(new XMLBuilder({attributeNamePrefix:'@_'}).build(JSON.parse(require('fs').readFileSync('in.json','utf8'))))" or an equivalent Python script with xmltodict.
**Q: What JSON types are supported?**
A: All six JSON types are supported. Objects become XML elements with child elements. Arrays become repeated same-named sibling elements. Strings, numbers, booleans, and null become element text content. Booleans and null are serialized as their literal string representations: true, false, and empty content for null. No type coercion is applied — numbers are written to XML text content exactly as they appear in the JSON, preserving decimals and precision.
---
### JSON to YAML Converter
URL: https://go-tools.org/tools/json-to-yaml
Paste JSON, get YAML instantly. Live conversion in your browser. K8s/Compose-ready, 2/4-space indent, smart quoting. 100% private, no upload.
#### What is YAML and Why Convert from JSON?
YAML (YAML Ain't Markup Language) is a human-readable data serialization format designed for configuration files, infrastructure-as-code, and anywhere a human writes data that a machine will read. Its indentation-based syntax requires no braces or brackets, making it far more legible than JSON for complex nested structures. Kubernetes, Helm, Ansible, Docker Compose, GitHub Actions, CircleCI, and virtually every cloud-native tool uses YAML as its primary configuration format. Converting JSON to YAML is therefore one of the most common tasks in DevOps and backend development — you receive a resource definition from an API in JSON, and you need a YAML manifest to commit to version control.
This tool has four important differentiators compared to typical online converters:
**1. Norway Problem — Auto-Safe Quoting.** The single biggest footgun in JSON-to-YAML conversion is the YAML Norway Problem. In YAML 1.1 (which millions of production parsers still use, including older Kubernetes, PyYAML, Ansible, and Ruby's Psych), the bare strings yes, no, on, off, y, and n are parsed as boolean true/false values. This famously bit the ISO country code for Norway ("NO" → false) and has caused real production outages in Kubernetes configs. YAML 1.2 fixed this, but your parsers may not be on 1.2. This tool's default Auto quote mode uses the eemeli/yaml library with the YAML 1.1 schema, so it automatically wraps any Norway-problem string in quotes, guaranteeing safe round-trips through both YAML 1.1 and 1.2 parsers. Learn more in our companion article at The YAML Norway Problem and JSON-YAML Differences .
**2. Key Order Preservation.** Unlike some converters that sort keys alphabetically, this tool preserves the original key insertion order from your JSON — matching the behavior of JSON.parse() in all modern JavaScript engines. This matters for Kubernetes manifests (where apiVersion and kind are expected first by convention), OpenAPI specs (where info appears before paths), and any config where field ordering is meaningful for readability or diffs.
**3. Big-Number Precision Caveat.** JSON numbers larger than 2^53 - 1 (9007199254740991) cannot be represented exactly in JavaScript's IEEE 754 double-precision float. When JSON.parse() reads a large integer like a Kubernetes resourceVersion field (which is a 64-bit integer on the server), it silently truncates it. This is a fundamental browser JavaScript limitation that affects every browser-based JSON tool, including this one. The safe workaround is to ensure large integers are stored as strings in your JSON before converting. This tool documents this behavior honestly in the Number Precision Loss common error below.
**4. 100% Browser-Based Privacy.** Your JSON data — which often contains API keys, database credentials, internal service configurations, and production secrets — never leaves your browser. No data is sent to any server. You can verify this in your browser's Network tab. This is the only safe way to handle sensitive configuration data in an online tool. See our companion tool for the reverse direction at YAML to JSON Converter , and our JSON Formatter if you need to validate and pretty-print JSON before converting.
YAML's human-readable nature comes with a tradeoff: it has more parsing edge cases than JSON. Beyond the Norway Problem, YAML has octal number quirks (0777 is parsed as 511 in YAML 1.1), multiline string syntax (| for literal, > for folded), anchor and alias references (&anchor and *alias), and multiple document support (--- separator). JSON has none of these complexities — it is a strict, minimal format with only six data types. For machine-to-machine data exchange, JSON is almost always the better choice. For human-edited configuration files where readability and comments matter, YAML wins. This converter gives you the best of both: use JSON programmatically, convert to YAML for your infrastructure. Need to compare two JSON documents and find what changed? Try our JSON Diff . If your destination is a spreadsheet rather than YAML, use JSON to CSV (or the reverse CSV to JSON ) instead.
```
// Convert JSON to YAML in Node.js using the eemeli/yaml library
import { Document } from 'yaml';
const data = JSON.parse('{"apiVersion":"apps/v1","kind":"Deployment"}');
// version: '1.1' ensures Norway-problem strings (yes/no/on/off/y/n)
// are automatically quoted in the output for YAML 1.1 parser safety
const doc = new Document(data, { version: '1.1' });
const yamlString = doc.toString({
indent: 2,
lineWidth: 0, // disable line wrapping
defaultStringType: 'PLAIN', // Auto mode: only quote when needed
});
console.log(yamlString);
// apiVersion: apps/v1
// kind: Deployment
```
#### FAQ
**Q: How do I convert JSON to YAML online?**
A: Paste your JSON into the input field above. The tool converts it to YAML instantly in your browser — no button click needed. You can adjust indentation (2 or 4 spaces), quoting style (Auto, Double, or Single), and output style (Block or Flow) from the Options panel. Once the YAML appears in the output area, click Copy to grab it to your clipboard or Download to save it as a .yaml file. Everything runs locally — your data never leaves your device.
**Q: What is the YAML Norway Problem and how does this tool handle it?**
A: The YAML Norway Problem refers to a quirk in YAML 1.1 specification where bare strings like "no", "yes", "on", "off", "y", and "n" are parsed as boolean values (false/true) instead of strings. This caused a famous real-world issue where the ISO country code for Norway ("NO") was misread as the boolean false in Ansible playbooks and Kubernetes configs. In YAML 1.2, this was fixed — bare strings are always strings. However, millions of production parsers (older Kubernetes versions, PyYAML, Ansible, Ruby's Psych) still use YAML 1.1. This tool's Auto quote mode (the default) automatically wraps any Norway-problem strings in quotes so they round-trip safely through both YAML 1.1 and 1.2 parsers. When Norway-problem strings are detected in your input, a warning banner lists exactly which values were auto-quoted.
**Q: Why does the Norway Problem matter for Kubernetes and DevOps?**
A: Kubernetes YAML manifests, Helm chart values, Ansible playbooks, and GitHub Actions workflows are all parsed by tools that historically used YAML 1.1. If you have a config key with the value "no" (for example, a country code, an enabled flag in string form, or a custom boolean-like field), a YAML 1.1 parser will silently convert it to the boolean false. This can cause service misconfigurations that are extremely difficult to debug because the YAML appears correct when viewed as text but behaves differently when parsed. Always use Auto quote mode when converting JSON for use in Kubernetes or any DevOps toolchain to guarantee safe round-trips.
**Q: Should I use 2-space or 4-space indentation for YAML?**
A: Use 2-space indentation for Kubernetes manifests, Helm values, Docker Compose files, and GitHub Actions workflows — these tools are designed around 2-space YAML and it is the community convention. Use 4-space indentation for Ansible playbooks (which follow a 4-space convention) and when your team or organization has a style guide mandating it. YAML forbids tabs entirely — all indentation must be spaces. This tool defaults to 2 spaces, which is the correct choice for the vast majority of cloud-native use cases.
**Q: How do I use this tool to create a Kubernetes manifest?**
A: If you have a Kubernetes resource definition in JSON (from kubectl get deployment my-app -o json, an API response, or a Terraform resource block), paste it into the input field. Select 2 spaces indentation (the default) and Auto quotes (the default, which handles the Norway Problem). The YAML output is immediately ready for kubectl apply -f. You can also click Download to save the file with a .yaml extension and pipe it directly into kubectl apply -f -. The K8s Deployment example above shows a complete deployment manifest you can load and modify.
**Q: How do I convert a Docker Compose JSON to YAML?**
A: Paste your Docker Compose JSON into the input field. Use 2-space indentation (Docker Compose convention) and Block style. The output YAML is compatible with docker compose up, docker compose config, and Docker Stack. A common scenario is exporting a running stack's configuration with docker inspect and then converting it back to a compose.yaml file. The Docker Compose example above includes service definitions with ports, environment variables, volumes, and depends_on.
**Q: Can JSON numbers larger than 2^53 lose precision when converting to YAML?**
A: Yes. This is a fundamental JavaScript limitation: the IEEE 754 double-precision float used by JavaScript's Number type can only represent integers exactly up to 2^53 - 1 (9007199254740991). Any integer beyond that — such as Kubernetes resourceVersion fields (which are int64 on the server) — will be silently rounded when parsed by JSON.parse(). For example, the value 9007199254740993 becomes 9007199254740992 in JavaScript, and this truncated number will appear in your YAML output. This affects all browser-based JSON tools, not just this one. The safe workaround is to store large integers as strings in your JSON ("resourceVersion": "9007199254740993") — they will appear as YAML strings without any precision loss.
**Q: Does the converter preserve the original key order from my JSON?**
A: Yes. The eemeli/yaml library used by this tool preserves insertion order, which matches the behavior of JSON.parse() in all modern JavaScript engines (V8, SpiderMonkey, JavaScriptCore). Keys appear in the YAML output in the same order they appeared in the JSON input. This is important for Kubernetes manifests and OpenAPI specs where field ordering is often meaningful for readability and diffs.
**Q: When should I use JSON versus YAML?**
A: Use JSON when: you are building APIs and web services (JSON is the universal interchange format), when machine parsing speed matters, when you need strict type safety, or when the consumer is a JavaScript/TypeScript application. Use YAML when: writing configuration files intended for human editing (Kubernetes manifests, CI/CD pipelines, Ansible playbooks, Helm values), when you want comments in your config, or when readability is more important than strictness. A helpful rule: if a machine writes it or reads it first, use JSON; if a human writes it and a machine reads it, use YAML.
**Q: How can I convert JSON to YAML on the command line?**
A: The most popular approach is combining yq and jq. Install yq (Mike Farah's version, not the Python one): brew install yq on macOS or wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq for Linux. Then run: cat input.json | yq -P to pretty-print as YAML. Alternatively: yq -o yaml input.json or cat input.json | python3 -c "import sys, json, yaml; yaml.dump(json.load(sys.stdin), sys.stdout, default_flow_style=False)". For Kubernetes specifically: kubectl get deployment my-app -o yaml converts directly from the cluster API.
**Q: How do I convert JSON to YAML in Python, Node.js, or Go?**
A: In Python: import json, yaml; yaml.dump(json.load(open('input.json')), open('output.yaml', 'w'), default_flow_style=False) using PyYAML, or ruamel.yaml for round-trip fidelity. In Node.js: import { Document } from 'yaml'; const doc = new Document(JSON.parse(input), { version: '1.1' }); const result = doc.toString({ indent: 2, lineWidth: 0 }) — this is the same library and approach used by this tool. In Go: import gopkg.in/yaml.v3; json.Unmarshal(jsonBytes, &data); yaml.Marshal(data) — note that Go YAML v3 uses YAML 1.1 by default, so Norway-problem strings will be auto-quoted.
**Q: Is my JSON data sent to any server when I use this tool?**
A: No. All conversion happens entirely in your browser using JavaScript. Your JSON data is never transmitted over the network, never stored on any server, and never logged or analyzed. This makes the tool safe to use with API keys, database credentials, internal configuration files, production Kubernetes manifests, and any other sensitive data. The tool uses no cookies for your input data and no third-party analytics that would capture your paste. You can verify this by opening your browser's Network tab — you will see zero requests triggered by pasting JSON.
**Q: Is there a file size limit for JSON input?**
A: There is no hard file size limit, but large inputs (over 200KB) automatically switch from live conversion to manual mode. In manual mode, a Convert button appears and conversion runs only when you click it — this prevents the browser's main thread from blocking for 200-500ms on every keystroke. For very large JSON files (multi-megabyte), consider using command-line tools like yq or jq for better performance. The tool efficiently handles typical real-world payloads like full Kubernetes namespace dumps, large OpenAPI specs, and multi-service Docker Compose files.
---
### JSON to Zod Schema Converter
URL: https://go-tools.org/tools/json-to-zod
Paste JSON, get a ready-to-use Zod schema instantly, 100% in your browser. Correct z.number().int(), .optional() and .nullable(), plus a z.infer type. Free.
#### What is a Zod schema?
A Zod schema is a TypeScript-first description of a value's shape that validates data at runtime and infers a static type at compile time. Generating one from a JSON sample means you never hand-write validation boilerplate for API responses, forms, or config files. This Zod schema generator infers correct types, marks absent keys as .optional(), null values as .nullable(), and hands you a z.infer type — all 100% in your browser.
#### FAQ
**Q: How do I convert JSON to a Zod schema?**
A: Paste your JSON into the box on the left. The converter parses it instantly in your browser and generates a Zod schema on the right, together with a z.infer type alias. Click Copy to grab it — no upload, no account, no waiting.
**Q: What is the difference between JSON to Zod and JSON Schema to Zod?**
A: This tool takes a sample JSON value — an API response or object — and infers a Zod schema from its shape. JSON Schema to Zod is a different task: it converts an existing JSON Schema document into Zod. If you have raw data, use this tool. If you already have a JSON Schema file, convert that instead.
**Q: How do I get a TypeScript type from the schema?**
A: Every result includes a z.infer type alias, so you get a fully typed Root without writing it by hand. Keep the schema as the single source of truth and let TypeScript derive the type, so the two never drift apart. You can toggle the z.infer line off in Options if you only want the schema.
**Q: How are optional and null fields handled?**
A: When a key appears in some array items but not others, its field is marked .optional(). A field that is null in some samples and typed in others becomes .nullable(). A field that is only ever null becomes z.null(). Zod treats optional and nullable independently, so the schema matches how your data actually varies.
**Q: What number type does it generate?**
A: Whole numbers map to z.number().int(); any value written with a decimal point or exponent maps to z.number(). If a field mixes integers and decimals across samples, it unifies to z.number() so validation never rejects a legitimate value.
**Q: Does the output work with Zod 3 and Zod 4?**
A: Yes. The generated code uses the common, stable subset — z.object, z.string, z.number, z.array, z.union, .int, .optional, .nullable and z.infer — that behaves the same in Zod 3 and Zod 4. Just make sure the zod package is installed in your project.
**Q: How do I validate data with the generated schema?**
A: Import the schema and call RootSchema.parse(data) to throw on invalid input, or RootSchema.safeParse(data) to get a typed success or error result without throwing. This is ideal at trust boundaries such as API responses, form input, and environment config.
**Q: How are arrays and mixed types handled?**
A: An array of one type becomes z.array of that type. An array of objects merges key by key into a single element schema. An array that mixes primitive types becomes a z.union, and anything more ambiguous falls back to z.unknown so you can refine it from a richer sample.
**Q: Is my JSON data private and safe?**
A: Yes. Conversion runs 100% in your browser with JavaScript. Your JSON — including tokens, IDs, or customer data — never leaves the page and is never sent to a server.
**Q: Is the tool free? Do I need an account?**
A: It is completely free with no sign-up, no limits, and no ads cluttering the workspace.
---
### JSON Unescape
URL: https://go-tools.org/tools/json-unescape
Unescape a JSON string back to readable text in your browser. Decodes \n, \t, \", \\, and \uXXXX, with or without surrounding quotes. 100% private, no upload.
#### What is JSON Unescaping and When Do You Need It?
JSON unescaping is the reverse of JSON escaping: it takes a string full of escape sequences — \n, \t, \", \\, \uXXXX — and turns each one back into the character it represents, recovering the original text. Where escaping makes a string safe to store inside a JSON document, unescaping makes a stored string readable again.
The need shows up constantly in debugging and data work. You copy a field out of a structured log and it is full of \n and \" that hide the real message — unescaping reveals the actual multi-line text. An API stored a request body as a string (JSON-in-JSON), and you need to read the inner object — unescaping turns {\"a\":1} back into {"a":1}. A legacy system emitted ASCII-safe output where every accent became \uXXXX — unescaping restores café and résumé. In each case the data is technically intact but unreadable until decoded.
This tool is built for that decode path with three advantages. First, it is lenient about the surrounding quotes: paste a full literal or just the escaped body, and it does the right thing — because escaped strings are usually copied out of context. Second, it decodes \uXXXX correctly, combining surrogate pairs into proper astral characters like emoji, identical to a compliant JSON parser, so anything escaped by a serializer round-trips perfectly. Third, it runs 100% in your browser, so the log fields and payloads you decode — which often contain PII or secrets — never reach a server. To re-escape afterward, use our JSON Escape tool; to validate the decoded JSON, see the JSON Formatter .
```
// Escaped input (copied from a log, quotes optional)
User said: \"it works!\"\nSession ended.
// Unescaped output — readable again
User said: "it works!"
Session ended.
// \uXXXX and surrogate pairs decode too
caf\u00e9 \ud83d\ude00 -> café 😀
// JSON-in-JSON
{\"a\":1} -> {"a":1}
```
#### FAQ
**Q: What does this JSON unescape tool do?**
A: It reverses JSON escaping: it takes a JSON-escaped string and decodes the escape sequences back into the characters they represent, entirely in your browser. \n becomes a real newline, \t a tab, \" a double quote, \\ a single backslash, \/ a forward slash, and \uXXXX the corresponding Unicode character (including surrogate pairs for emoji and astral scripts). The result is the original, human-readable text. You can paste the string with or without its surrounding double quotes — the tool detects and handles both. Everything runs client-side, so escaped payloads containing sensitive data never leave your machine.
**Q: Do I need to include the surrounding double quotes?**
A: No — the tool accepts both forms. If you paste a complete JSON string literal like "hello\nworld" (with the outer quotes), it is parsed directly. If you paste just the escaped body hello\nworld (no outer quotes), the tool wraps it for you before decoding. This is convenient because escaped strings are often copied out of the middle of a larger document, where the surrounding quotes were left behind. Either way you get the same decoded text.
**Q: Is my data uploaded anywhere?**
A: No. All decoding runs entirely in your browser using JavaScript — the escaped string you paste is never transmitted, stored, logged, or analyzed on any server. This makes the tool safe for decoding log fields, webhook payloads, and config values that may contain PII or secrets. You can confirm it in your browser's Network tab: pasting triggers zero network requests. No cookies capture your input and no third-party analytics read what you paste.
**Q: Why do I get an 'invalid escape sequence' error?**
A: The error means the input is not a valid JSON-escaped string, so it cannot be decoded unambiguously. The most common cause is a lone backslash followed by a character JSON does not recognize as an escape — for example \q or \x41 (JSON has no \x hex escape; it uses \u). Another cause is an unbalanced or stray double quote inside an unquoted input, which breaks the automatic wrapping. Check that every backslash starts a valid escape (\n \t \r \b \f \" \\ \/ \uXXXX) and that quotes are properly paired.
**Q: How do I read a JSON object that was stored as a string (JSON-in-JSON)?**
A: Paste the escaped string — for example {\"a\":1} — and the tool decodes it back to the real JSON {"a":1}, which you can then read or copy into a parser. This double-decoding is exactly what you need when a webhook envelope, message-queue record, or audit log stored a request body as an escaped string field. After unescaping, paste the result into our JSON Formatter to pretty-print and validate it. To go the other direction and escape JSON for embedding, use the JSON Escape tool.
**Q: Does it correctly decode \uXXXX and emoji?**
A: Yes. Each \uXXXX is decoded to its UTF-16 code unit, and consecutive high/low surrogate escapes are combined into the correct astral character — so \ud83d\ude00 becomes 😀 and \u00e9 becomes é. This is the same decoding any compliant JSON parser performs, which means a string escaped by our JSON Escape tool (or any serializer) round-trips back to the exact original here, byte for byte. If the escape you are holding has a different shape — a `U+` code point, an HTML entity, or the bare `u4e2d` that is left over when a log pipeline swallows the backslash — our Unicode converter recognises those too and turns them back into readable text.
---
### Free JSONPath Tester — Evaluate Queries Online
URL: https://go-tools.org/tools/jsonpath-tester
Test JSONPath expressions against any JSON, 100% private in your browser — no upload, no signup, no eval. RFC 9535 standard engine plus Classic Goessner mode, with Values, Paths and Both result views.
#### What is a JSONPath tester?
A JSONPath tester is a tool that lets you write a JSONPath expression, paste a JSON document, and see exactly which nodes the expression selects — both the matched values and their precise locations — without writing code or running a script. For developers it shortens the loop from minutes to milliseconds: tweak the path, watch the result change, and ship the query with confidence.
JSONPath is a query language for JSON, the JSON analogue of XPath for XML. An expression is built from a small alphabet of selectors. $ is the root of the document. A dot or a bracket steps into a child: $.store or $['store']. The double dot .. is recursive descent — it searches every level of the tree. The wildcard * selects all elements or members. Brackets carry array indices ([0]), slices ([start:end:step]), unions ([a,b]), and filter expressions ([?(@.price < 10)], where @ is the element being tested). With those pieces you can pull a single field out of a deeply nested API response, assert on values in tests, drive data transforms in systems like Kubernetes, AWS Step Functions, and Azure Logic Apps, or extract structured data from irregular JSON — all without imperative traversal code. JSONPath is also famously inconsistent between implementations, which is exactly the problem a good tester surfaces before it reaches production.
This tester ships two engines. The default is an RFC 9535 engine: RFC 9535 is the IETF's February 2024 formal specification of JSONPath, the first time the language was precisely standardized after fifteen years of divergent implementations. It defines an exact grammar, the concept of normalized paths for results, and five standard functions — length(), count(), match(), search(), value(). Our RFC 9535 engine is a zero-dependency implementation that uses no eval, so it parses and interprets expressions with its own grammar instead of compiling them to JavaScript. The second engine is Classic (Goessner), the de facto 2007 dialect that most older online tools and libraries implement; switch to it to reproduce results from a tool like jsonpath.com or to run an expression you copied from legacy code. The two dialects agree on common paths but diverge in the edge cases — filter whitespace and quoting, union ordering, how missing members compare, and which functions exist — so being able to flip between them in one place is the fastest way to diagnose why an expression behaves differently than you expected.
What the tester surfaces beyond raw values: the result of a JSONPath query is a nodelist, and this tool can show it three ways. The Values view renders the matched nodes as a JSON array, exactly what you would consume in code. The Paths view renders each match's normalized path — a canonical, bracket-quoted location such as $['store']['book'][0]['title'] that uniquely identifies where in the document the value lives, no matter how the expression was written. Two expressions that select the same node produce the same normalized path, which makes the Paths view invaluable for debugging. The Both view shows values and paths side by side. A stats line reports how many nodes matched.
Security is a first-class concern here. Many online JSONPath evaluators run on a server, or embed a library that evaluates filter predicates with JavaScript eval — the design that produced remote-code-execution vulnerabilities tracked as CVE-2024-21534 and CVE-2025-1302 in widely used JSONPath packages. This tool uses no eval at all. The RFC 9535 engine has no eval path, and the Classic engine is built on a patched, pinned release of jsonpath-plus with eval explicitly disabled. That closes the RCE class of bugs and lets the tool run under a strict Content-Security-Policy that forbids unsafe-eval. Every evaluation is local: your JSON and your expression never leave the page, are never logged, and are never stored on disk — only your engine and view preferences persist to localStorage. That makes the tool safe for proprietary API payloads, redacted logs, internal config, and any data with a schema you would not paste into a server-backed service.
If JSON wrangling is your task, pair this with the other JSON tools on the site: format and pretty-print your input with the JSON Formatter , compare two documents with the JSON Diff , check a payload against a schema with the JSON Schema Validator , or turn a sample response into typed interfaces with JSON to TypeScript .
```
// The expression you build in this tester maps straight onto the
// RFC 9535 reference library used under the hood.
import { query, paths } from 'jsonpath-rfc9535';
const document = {
store: {
book: [
{ title: 'Sayings of the Century', author: 'Nigel Rees', price: 8.95 },
{ title: 'Sword of Honour', author: 'Evelyn Waugh', price: 12.99 },
{ title: 'Moby Dick', author: 'Herman Melville', price: 8.99 },
{ title: 'The Lord of the Rings', author: 'J. R. R. Tolkien', price: 22.99 }
]
}
};
// Values: query(document, path) returns the matched values directly.
const titles = query(document, '$.store.book[*].title');
// → ['Sayings of the Century', 'Sword of Honour', 'Moby Dick', 'The Lord of the Rings']
// Filter: books cheaper than 10.
const cheap = query(document, '$.store.book[?(@.price < 10)].title');
// → ['Sayings of the Century', 'Moby Dick']
// Normalized paths: paths(document, path) returns where each match lives.
const authorPaths = paths(document, '$..author');
// → ["$['store']['book'][0]['author']", "$['store']['book'][1]['author']", ...]
// RFC 9535 functions like length() are used INSIDE filters, not as a segment.
const longTitles = query(document, '$.store.book[?length(@.title) > 15]');
// → the two books whose title is longer than 15 characters
```
#### FAQ
**Q: Is my JSON or JSONPath expression sent to your server?**
A: No. Every evaluation runs in JavaScript inside your browser. Your JSON document and your JSONPath expression are not uploaded, not logged, not stored on disk, and not sent to any third party. Only your UI preferences — the active engine (RFC 9535 or Classic) and the result view (Values / Paths / Both) — are saved to localStorage so the page remembers them next visit; the JSON and the expression themselves are never persisted. You can verify by opening DevTools → Network: typing in either box fires zero requests. That makes this tool safe for proprietary API payloads, redacted log samples, internal config, and anything else you would not paste into a server-backed evaluator like jsonpath.com.
**Q: What is JSONPath and what is it used for?**
A: JSONPath is a query language for JSON, the same way XPath is a query language for XML. You write a path expression — for example $.store.book[*].author — and the evaluator returns every value in the document that the path selects. It is used to pull specific fields out of API responses, to assert on values in integration tests, to configure data transforms in tools like Jenkins, Kubernetes, AWS Step Functions, and Azure Logic Apps, and to extract data from large or irregular JSON without writing imperative traversal code. An expression is built from an axis of selectors: $ (the root), . or [] (child access), .. (recursive descent), * (wildcard), [start:end:step] (array slice), [a,b] (union), and [?()] (filter). This tester evaluates the expression live and shows both the matched values and their normalized paths.
**Q: What is the difference between RFC 9535 and the classic Goessner syntax?**
A: Classic JSONPath is the de facto syntax Stefan Goessner published in 2007. It became widely implemented but was never formally standardized, so subtle behaviors — how filters are written, how unions and the root function work, how absent values compare — diverged across libraries. RFC 9535, published by the IETF in February 2024, is the first formal specification of JSONPath. It nails down a precise grammar, defines normalized paths for results, and adds standard functions (length, count, match, search, value). The two are close but not identical: RFC 9535 is stricter about whitespace and quoting in filters, defines comparison semantics for missing members, and rejects some loose constructs the classic dialect tolerated. This tool defaults to the RFC 9535 engine (a zero-dependency, no-eval implementation) and lets you switch to a Classic (Goessner) engine for backward compatibility.
**Q: Why does the same expression return different results in the two engines, and how do I use an expression copied from jsonpath.com?**
A: Because RFC 9535 and the classic Goessner dialect have genuinely different rules in the edge cases — filter whitespace and quoting, union ordering, how missing members compare, and which functions exist. An expression written for one engine can match differently (or fail to parse) in the other. If you copied an expression from an older tool such as jsonpath.com, jsonpath-plus, or a Jayway-based service, switch the engine toggle at the top to Classic (Goessner): that mode runs a Goessner-compatible evaluator (built on jsonpath-plus, constructed with eval disabled) and will reproduce the behavior you saw in the source tool. If you are writing a new expression or targeting a system that advertises RFC 9535 compliance, keep the default RFC 9535 engine. The cheat sheet and built-in examples are written to evaluate identically in both engines so you have a known-good starting point.
**Q: How do filter expressions [?()] work?**
A: A filter selector keeps only the array elements (or object members) for which a predicate is true. Inside the filter, @ refers to the current element being tested. $.store.book[?(@.price < 10)] returns every book whose price member is less than 10. You can compare against literals (@.isbn, @.category == 'fiction'), combine conditions with && and ||, test for the existence of a member (@.isbn selects elements that have an isbn at all), and in RFC 9535 use the function extensions inside the predicate (?(length(@.tags) > 2)). Comparison operators are ==, !=, <, <=, >, >=. RFC 9535 is precise about types: comparing a missing member to a value is well-defined and does not throw. The classic dialect is looser about whitespace, so [?(@.price<10)] and [?(@.price < 10)] are both accepted there; RFC 9535 follows its grammar exactly.
**Q: What does recursive descent (..) do?**
A: The .. operator searches every level of the document, not just the immediate children. $..author collects every author member wherever it occurs — inside the top-level object, inside arrays, inside nested objects, at any depth. It is the fastest way to extract a field from a deeply nested or irregularly shaped structure when you do not want to (or cannot) spell out the full path. You can follow .. with any selector: $..book[*] finds every element of every book array anywhere in the tree, $..* enumerates every value in the document, and $..['price'] gathers all price members. Recursive descent can match a lot — switch to the Paths view to see exactly where each result came from via its normalized path.
**Q: What are the RFC 9535 functions length(), count(), match(), search(), and value()?**
A: RFC 9535 defines five standard function extensions, and the key rule is that they are only callable inside a filter expression [?...] — never as a standalone path segment. Writing $.store.book.length() is not valid RFC 9535 and the standard grammar rejects it (that segment-call form is a jsonpath-plus extension, not part of the spec). length() returns the length of a string, array, or object, so you use it to filter by size: $.store.book[?length(@.title) > 15] keeps books whose title is longer than 15 characters. count() returns the number of nodes a nodelist contains, again inside a filter: $.store.book[?(count(@.authors) > 1)]. match() tests whether a string matches a regular expression against the whole value, and search() tests for a match anywhere within the string — both take an I-Regexp pattern. value() converts a single-node nodelist to its value so it can be used in a comparison. These functions are part of the RFC 9535 standard, so they are available in the default engine; the Classic (Goessner) engine does not implement them. If a function-based expression fails, confirm you are calling it inside a filter and that the engine toggle is set to RFC 9535.
**Q: How do array slices [start:end:step] work?**
A: Slices use the same half-open convention as Python and JavaScript: [start:end] selects from index start up to but not including index end, so [0:2] returns the first two elements (indices 0 and 1). Omit a bound to run to the edge — [2:] from index 2 onward, [:3] for the first three. A negative index counts from the end: [-1:] selects the last element. The optional third field is a step — [::2] takes every other element, [::-1] reverses (in engines that support negative steps). The exclusive end bound is the single most common slice bug; the Paths view shows the exact index of every selected element so you can confirm the boundary at a glance.
**Q: What is a union selector and how do I select multiple keys at once?**
A: A union selector lists several names or indices inside one bracket and gathers all of them: $['title','author'] selects both members from an object, and $.store.book[0,2] selects the first and third elements of the book array. You can mix it with other selectors — $.store.book[*]['title','price'] pulls the title and price of every book. Unions are handy when you want a fixed projection of a few fields rather than a whole object or a wildcard. The Both view is the clearest way to read a union result because it pairs each selected value with its normalized path, so you can tell which name or index produced each entry.
**Q: Can I share a JSONPath query and its JSON via a link?**
A: Yes — and the link involves no server roundtrip. Click Copy link in the action bar: the tester encodes the JSON, the expression, the active engine, and the result view into the URL hash. Anyone who opens the link hydrates the page with the same state, locally on their own machine. Because the data lives in the hash fragment, it is never transmitted to the go-tools.org server — browsers do not send the fragment in HTTP requests — and it never appears in our access logs. The link length grows with the size of the JSON, so for large documents share just the expression and let the recipient paste their own data, or use the Upload button to load a file locally. This makes permalinks safe for collaborative debugging without exposing the payload to any backend.
**Q: Is there a maximum JSON size?**
A: Evaluation is bounded by your browser's memory rather than a hard cap, but the practical sweet spot is documents up to a few megabytes — comfortably larger than almost any single API response. Very large arrays with broad selectors (a $..* recursive wildcard over tens of thousands of nodes) will produce a large result set that takes longer to render; narrow the expression to keep the output readable. The Upload button reads a .json or .txt file entirely in the browser (it is never sent anywhere), and Format JSON re-indents the input so you can read the structure before querying it. For multi-megabyte data pipelines, validate your expression here against a representative slice, then run the same path in your application code or in a CLI tool like jq.
**Q: How is this different from jsonpath.com and is it safe — no eval?**
A: Four differences. (1) Privacy: jsonpath.com and most online evaluators run on a server or embed a library that evaluates filters with JavaScript eval; this tool runs entirely in your browser and uses no eval at all. The default RFC 9535 engine is a zero-dependency implementation with no eval path, and the Classic (Goessner) engine is built on jsonpath-plus pinned to a patched release with eval explicitly disabled — closing the remote-code-execution class of bugs tracked as CVE-2024-21534 and CVE-2025-1302. That also means the tool works under a strict Content-Security-Policy. (2) Standards: this is one of the few online testers offering a true RFC 9535 engine, not just the legacy Goessner dialect. (3) Dual-engine: you can switch between RFC 9535 and Classic to compare results or to run expressions copied from older tools, side by side. (4) Languages: the interface is available in 15 languages. If you only need quick legacy-syntax checks, jsonpath.com still works; for standards-compliant, private, no-eval evaluation, this is the safer choice.
**Q: What do the Values, Paths, and Both views show?**
A: The result of a JSONPath query is a nodelist — a set of nodes inside your document. The Values view renders those nodes as a JSON array of the matched values, exactly what you would consume in code. The Paths view renders the normalized path of each match instead — a canonical, bracket-quoted location like $['store']['book'][0]['title'] that uniquely identifies where in the document the value lives, regardless of how your expression was written. Normalized paths are an RFC 9535 concept and are invaluable for debugging: two different expressions that select the same node produce the same normalized path. The Both view shows the two side by side so you can match each value to its location at a glance. Your chosen view persists across sessions via localStorage.
**Q: Does this work offline, and what about a Content-Security-Policy?**
A: Yes on both counts. Because every evaluation runs in your browser with no network calls, the tool keeps working once the page has loaded even if you go offline. And because neither engine uses eval or the Function constructor to evaluate filter expressions, the tool runs under a strict Content-Security-Policy that forbids unsafe-eval — the policy that many security-conscious organizations enforce and that breaks eval-based JSONPath libraries. The RFC 9535 engine parses and interprets expressions with its own grammar rather than compiling them to JavaScript, and the Classic engine is configured with eval disabled. If you need to evaluate JSONPath inside a hardened internal environment, this tool is designed to run there without policy exceptions.
---
### JWT Decoder
URL: https://go-tools.org/tools/jwt-decoder
Decode JWT tokens online with our free JWT decoder. Instantly inspect header, payload, signature, expiration, algorithm, and claims. 100% browser-based — your token never leaves your device. No signup, no tracking.
#### What is a JWT?
A JSON Web Token, or JWT (pronounced 'jot'), is a compact, URL-safe token format for carrying claims between two parties. It is defined in RFC 7519 and is the dominant credential format used by OAuth 2.0 access tokens, OpenID Connect ID tokens, API keys in modern auth providers (Auth0, Okta, Clerk, Supabase, Firebase), and inter-service tokens in microservice architectures.
"JSON Web Token (JWT) is a compact claims representation format intended for space-constrained environments such as HTTP Authorization headers and URI query parameters." — RFC 7519, Section 1
A JWT is three Base64URL-encoded JSON objects joined by dots: header.payload.signature. The header describes how the token is signed (the alg claim — for example, HS256 or RS256 — and the typ claim, usually 'JWT'). The payload carries the claims: registered claims like iss, sub, aud, exp, iat, plus whatever custom claims the issuer needs (role, scope, email, tenant ID). The signature is a cryptographic proof, computed over the header and payload with the issuer's secret or private key, that lets the recipient detect tampering.
Crucially, a JWT is encoded, not encrypted . Anyone with the token can read its payload — decoding is just Base64URL and JSON parsing. The security guarantee comes from the signature: an attacker can read a JWT, but cannot produce a different JWT that passes signature verification without the signing key. That is why JWTs are safe to pass over the network, but unsafe to fill with secrets.
A JWT decoder shows you exactly what a token contains — algorithm, claims, expiration — without touching the signature. It is the fastest way to answer 'is this token expired?', 'what role does this user have?', 'which issuer minted this token?', or 'is this an alg:none token I should reject?'. All decoding in this tool runs locally in your browser, so pasting a live production token is safe.
JWT work often pairs with other developer tools. You may need to decode a Base64URL-wrapped segment when debugging a malformed token, URL-decode an Authorization header after capturing it from a proxy, or convert the exp claim to a human date manually. For a deeper walkthrough of how JWTs are signed, verified, and rotated in production, see our Base64 fundamentals guide — Base64URL is the foundation every JWT is built on.
```
// Decode a JWT in the browser — header & payload only
function decodeJwt(token) {
const [h, p, s] = token.split('.');
const pad = (seg) => seg + '==='.slice((seg.length + 3) % 4);
const decode = (seg) => JSON.parse(
atob(pad(seg).replace(/-/g, '+').replace(/_/g, '/'))
);
return { header: decode(h), payload: decode(p), signature: s };
}
const { header, payload } = decodeJwt(token);
console.log(header); // → { alg: 'HS256', typ: 'JWT' }
console.log(payload); // → { sub: 'user_123', exp: 1999999999, ... }
// Expiration check
const expired = payload.exp * 1000 < Date.now();
```
#### FAQ
**Q: How do I decode a JWT token online?**
A: Paste the full JWT — all three dot-separated segments (header.payload.signature) — into the decoder above. Decoding happens instantly in your browser: the header and payload are Base64URL-decoded to readable JSON, and the signature is displayed as a raw string. A status row surfaces the signing algorithm, issued-at time, and expiration, so you can spot an expired token at a glance. To decode a JWT manually, split the token on dots, Base64URL-decode the first two segments, and parse them as JSON — anyone with the token can read its claims because the payload is encoded, not encrypted . This decoder is safe to use with production tokens because nothing ever leaves your device: no network request, no logging, no tracking.
**Q: What is a JWT (JSON Web Token)?**
A: A JSON Web Token (JWT) is a compact, URL-safe credential that carries claims between two parties. Defined in RFC 7519 , it consists of three Base64URL-encoded sections joined by dots: the header (algorithm and token type), the payload (claims — data about the user and the token itself), and the signature (a cryptographic proof that the token was issued by a trusted party). JWTs are the standard way to represent access tokens in OAuth 2.0 and ID tokens in OpenID Connect.
**Q: Is my token safe with this JWT decoder?**
A: Yes. All decoding runs in your browser using native JavaScript (atob and TextDecoder). Your token is never sent to a server, never logged, never stored, and never used for analytics. There are no cookies and no tracking. This matters because JWTs can contain live access tokens — pasting them into a remote debugger would be equivalent to handing over a credential. Our tool is safe to use with production tokens.
**Q: How does a JWT decoder work?**
A: A JWT decoder splits the token by dots into three parts, Base64URL-decodes the header and payload, and parses them as JSON. The signature is left as an opaque Base64URL string because verifying it requires the issuer's secret or public key — something a client-side decoder cannot do safely. This means decoding is instant and reveals the claims, but you must verify the signature server-side with the correct key before trusting anything inside.
**Q: Can this tool verify a JWT signature?**
A: No, and it intentionally does not. Signature verification requires the issuer's secret (for HMAC) or public key (for RSA/ECDSA/EdDSA), which should never be pasted into a public web tool. Verification must happen on your server, in your auth middleware, or inside an SDK that has access to your JWKS endpoint. This decoder is for inspecting what a token claims — it does not imply the token is authentic or untampered.
**Q: What are iat, exp, nbf, iss, aud, sub, and jti?**
A: These are the registered claims from RFC 7519. iat (issued at) is the Unix timestamp when the token was created. exp (expiration) is when the token stops being valid — this tool converts it to a human-readable date and marks the token as expired if exp is in the past. nbf (not before) is the earliest time the token can be used. iss (issuer) identifies who created the token. aud (audience) names the intended recipient. sub (subject) identifies the principal — usually a user ID. jti is a unique token ID used to prevent replay. Application-specific claims (role, scope, email, name) live alongside these.
**Q: My JWT is expired — why does the decoder still decode it?**
A: Decoding is not the same as validating. A JWT decoder reads the content regardless of expiration, so you can inspect an expired or otherwise invalid token to debug why it was rejected. The 'Expired' badge in this tool compares the exp claim against your local clock and flags tokens whose exp is in the past. A real authentication server would reject the token outright — but a decoder that refused to show expired tokens would be useless for debugging.
**Q: What is the difference between JWT, JWS, and JWE?**
A: JWT is the general concept — a JSON object encoded as a compact token. JWS (RFC 7515 ) is a signed JWT: the payload is readable by anyone who decodes it, and a signature proves it was not tampered with. This is by far the most common JWT you will encounter. JWE (RFC 7516 ) is an encrypted JWT: the payload itself is ciphertext and cannot be decoded without the decryption key. This tool decodes JWS tokens. A JWE token will decode only to its header — the encrypted payload is not readable without the key.
**Q: Why is alg:none dangerous?**
A: A JWT with alg:none has no signature — anyone can construct one, claiming to be any user. Early JWT libraries accepted alg:none by default, leading to a well-known class of authentication bypasses where an attacker would strip the signature, set alg to none, and forge an admin token. Every mature JWT library now rejects alg:none unless explicitly allowed, and you should never accept it for authenticated requests. This decoder will still show you an alg:none token, because inspecting one during debugging is legitimate — but treat any such token received in production as hostile.
**Q: Which algorithms does this JWT decoder support?**
A: Decoding the header and payload works for every algorithm, because decoding only needs Base64URL and JSON parsing — it is algorithm-agnostic. The tool correctly reads tokens signed with HS256, HS384, HS512 (HMAC), RS256, RS384, RS512 (RSA + SHA), PS256, PS384, PS512 (RSA-PSS), ES256, ES384, ES512 (ECDSA), EdDSA (Ed25519/Ed448), and unsigned tokens (alg:none). Only signature verification is algorithm-specific, and this tool does not perform verification.
**Q: Should I store JWTs in localStorage or cookies?**
A: Prefer HttpOnly, Secure, SameSite=Strict cookies for session tokens. A token in localStorage is readable by any JavaScript that runs on the page, so a single XSS vulnerability leaks every active session. HttpOnly cookies are invisible to JavaScript, which shrinks the blast radius of XSS to what an attacker can do within a live page — not a stolen token they can replay for days. If you must use localStorage (for example, for cross-domain apps), keep access token lifetimes short (minutes, not hours) and use a separate refresh token in an HttpOnly cookie.
**Q: How do I decode a JWT in Node.js, Python, or Go?**
A: Node.js: jsonwebtoken.decode(token) for read-only, jsonwebtoken.verify(token, key) for verification. Python: PyJWT.decode(token, options={'verify_signature': False}) to read, pass a key to verify. Go: jwt.ParseUnverified(token, claims) for read-only, jwt.Parse(token, keyFunc) for verification. In every language, never verify with options={'verify_signature': False} in production code — that is what this web tool does deliberately for debugging, and it is only safe when you are inspecting, not authenticating.
**Q: What is the maximum size of a JWT?**
A: The JWT standard does not impose a hard limit, but headers in most web servers default to around 8 KB. Keep tokens under 4 KB so they fit comfortably in Authorization headers and cookies. If your token is larger than that, you are probably putting too many claims in the payload — move bulky data behind an opaque session ID and fetch it from your backend when needed. Bloated JWTs also get costly on every request because they are sent with every API call.
**Q: I pasted my token and got 'Invalid JWT format' — what's wrong?**
A: A valid JWT has exactly three parts separated by dots: header.payload.signature. Common causes: (1) you accidentally copied only the payload segment, (2) whitespace or newlines were pasted in the middle, (3) the token was truncated in transit (common with terminal wrapping), (4) the token is a JWE where the format is header.encryptedKey.iv.ciphertext.tag (five segments), or (5) the token was URL-encoded and you need to URL-decode it first . Check the raw value your API returned — most editors show invisible characters on hover.
**Q: Can I decode a JWT without the secret key?**
A: Yes — the header and payload are Base64URL-encoded, not encrypted. Anyone who has the token can read its claims without any key. This is by design: the payload is meant to be readable so the recipient can make authorization decisions from it. The secret or public key is only required to verify that the token has not been tampered with. This is why you must never put sensitive data (passwords, private keys, PII beyond what the recipient already knows) inside a JWT payload.
**Q: My JWT works in Postman but my backend rejects it — how do I debug?**
A: Decode the token here and check: (1) exp — is it in the future relative to the server's clock? Server clock skew is a frequent culprit. (2) iss / aud — do they exactly match what your backend expects? A mismatch on aud is the most common false-negative. (3) alg — does your verification code allow that algorithm? An HS256 token will fail against a library configured for RS256 only. (4) kid — if you use key rotation, is the key ID in the header present in your JWKS? (5) signature — have you pasted the right secret/public key? This decoder surfaces (1), (2), (3), and (4) in the header and payload views so you can eliminate them quickly.
---
### JWT Encoder & Generator
URL: https://go-tools.org/tools/jwt-encoder
Free online JWT generator & encoder. Build the header and payload, sign with HS256, RS256, or ES256 instantly. 100% in-browser — your secret and key never leave your device.
#### What is a JWT Encoder?
A JWT encoder builds and cryptographically signs a JSON Web Token from a header and a payload of claims. A JWT, defined in RFC 7519 , is three Base64URL-encoded sections joined by dots: header.payload.signature. The header names the signing algorithm; the payload carries the claims (who the token is about, what it can do, when it expires); and the signature is a cryptographic proof, computed over the header and payload with a secret or private key, that lets a recipient detect tampering.
"JSON Web Token (JWT) is a compact claims representation format intended for space-constrained environments such as HTTP Authorization headers and URI query parameters." — RFC 7519, Section 1
Encoding is the inverse of decoding. A JWT decoder reads an existing token's claims; an encoder takes claims you supply and produces a brand-new signed token. The signing step is what separates a real JWT from arbitrary Base64 — without a valid signature, no verifier will accept the token. This tool signs using the browser's native Web Crypto API across the HMAC (HS), RSA (RS, PS), and ECDSA (ES) families, so the entire operation happens on your device with zero dependencies and zero network calls.
Developers reach for a JWT encoder constantly: to mint a token that exercises a protected API endpoint, to reproduce the exact claim shape an OAuth server issues so a bug can be debugged, to build fixtures for integration tests, or to hand a teammate a ready-to-use Bearer token for a curl command. Because the payload is encoded, not encrypted , a JWT is safe to pass over the network but must never carry secrets — anyone with the token can read every claim, and only the signature stops them from changing one.
JWT work pairs naturally with other developer tools. After signing, decode the token to confirm its claims, convert exp and iat between Unix time and human dates, or compute a SHA-256 hash when you need the underlying hash function that HS256's HMAC is built on. Because every JWT segment is Base64URL-encoded , a Base64 tool is handy when you inspect a token by hand; for an in-depth look at the encoding, see our Base64 fundamentals guide .
```
// Sign a JWT in the browser with the Web Crypto API (HS256)
async function encodeJwt(payload, secret) {
const b64url = (bytes) =>
btoa(String.fromCharCode(...new Uint8Array(bytes)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const enc = (obj) =>
b64url(new TextEncoder().encode(JSON.stringify(obj)));
const header = { alg: 'HS256', typ: 'JWT' };
const signingInput = `${enc(header)}.${enc(payload)}`;
const key = await crypto.subtle.importKey(
'raw', new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const sig = await crypto.subtle.sign(
'HMAC', key, new TextEncoder().encode(signingInput));
return `${signingInput}.${b64url(sig)}`;
}
const token = await encodeJwt({ sub: 'user_123', exp: 1999999999 }, 'my-secret');
// → eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0....
```
#### FAQ
**Q: How do I generate a JWT online?**
A: Edit the payload JSON in the box above, choose a signing algorithm (HS256 is the default and needs only a secret), and enter your secret or paste a PKCS8 PEM private key. The signed token appears instantly, with the header, payload, and signature segments color-coded so you can copy the whole thing with one click. Signing runs entirely in your browser using the native Web Crypto API — there is no Generate button to wait on and no request to a server, so it is safe to sign tokens with real keys during development.
**Q: What is a JWT generator?**
A: A JWT generator is a tool that builds and cryptographically signs a JSON Web Token from a header and a payload of claims, producing a header.payload.signature string you can use as a Bearer token. It is the inverse of a JWT decoder: instead of reading an existing token, it creates a new one signed with your secret (HS256) or private key (RS256/ES256). This generator runs entirely in your browser, so the token is produced instantly and your signing key never leaves your device.
**Q: Is this JWT generator free and safe to use?**
A: Yes — it is completely free, with no signup, no ads, and no tracking. It is safe because all signing happens locally in your browser via the Web Crypto API: your payload, secret, and private key are never uploaded, logged, or stored, and the tool makes no network requests at all. That makes it suitable even when you are working with sensitive keys, though using disposable test keys is always the safest habit.
**Q: Is it safe to enter my secret or private key here?**
A: Yes. Signing happens locally in your browser; your secret and private key are never sent to a server, never logged, never stored, and never used for analytics. There are no cookies and no tracking. This matters because a JWT signing key can mint valid credentials — pasting it into a remote tool would be equivalent to handing over the keys to your auth system. Because everything runs client-side, this encoder is safe to use with production keys, but you should still prefer disposable or test keys whenever possible.
**Q: What is the difference between HS256 and RS256?**
A: HS256 (HMAC-SHA256 ) uses a single shared secret to both sign and verify. It is simple and fast, but every party that can verify the token can also create one, so the secret must stay on trusted servers only. RS256 (RSA-SHA256) uses a key pair: you sign with a private key and others verify with the public key. This lets you distribute the public key freely — to client apps, partner services, or a JWKS endpoint — without giving anyone the ability to forge tokens. Use HS256 for symmetric, single-owner systems; use RS256 or ES256 when verifiers should not be able to mint tokens.
**Q: Which algorithms does this JWT encoder support?**
A: It signs with HS256, HS384, HS512 (HMAC with a shared secret), RS256, RS384, RS512 (RSA PKCS#1 v1.5), PS256, PS384, PS512 (RSA-PSS), and ES256, ES384, ES512 (ECDSA on P-256, P-384, and P-521). All of them are produced with the browser's native Web Crypto API, so there are no third-party libraries and nothing leaves your machine. HMAC algorithms take a text or Base64 secret; the RSA and ECDSA families take a PKCS8 PEM private key.
**Q: How do I set the exp (expiration) claim?**
A: Add an exp claim to the payload as a Unix timestamp in seconds — for example "exp": 1999999999. The quickest way is the exp +1h chip below the payload, which inserts an expiration one hour from now. You can also add iat (issued-at) and nbf (not-before) the same way. Remember that exp is in seconds , not milliseconds, and that verifiers compare it against their own clock, so keep server times in sync to avoid premature rejections. To convert a human date to a Unix timestamp, use our Unix timestamp converter .
**Q: How do I get a PKCS8 PEM private key for RS256 or ES256?**
A: For RSA: openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem. For ECDSA P-256: openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec-private.pem. Both commands emit a PKCS8 PEM block beginning with -----BEGIN PRIVATE KEY-----, which is exactly what this tool expects. Paste the whole block, including the header and footer lines. The matching public key — used to verify the token — can be derived with openssl pkey -in private.pem -pubout. If you would rather not touch the command line, our RSA key pair generator produces the same PKCS8 block in your browser, and can switch it to the traditional PKCS1 layout when something else needs that.
**Q: How do I verify the token I just generated?**
A: Paste it into our JWT decoder to confirm the header and payload decode as expected. To verify the signature , use your server or an SDK with the correct key: jwt.verify(token, secretOrPublicKey, { algorithms: ['HS256'] }) in Node.js, PyJWT.decode(token, key, algorithms=['RS256']) in Python, or jwt.Parse(token, keyFunc) in Go. Never verify with an empty algorithm list or with verify_signature=False in production — always pin the exact algorithm you expect.
**Q: What should I put in the payload?**
A: Keep it lean. The registered claims from RFC 7519 are iss (issuer), sub (subject — usually a user ID), aud (audience), exp (expiration), nbf (not before), iat (issued at), and jti (token ID). Alongside these you can add application claims like role, scope, or email. Do not put secrets in the payload — a JWT is encoded, not encrypted , so anyone with the token can read every claim. Keep tokens under about 4 KB so they fit in Authorization headers and cookies.
**Q: Is a JWT encrypted?**
A: No. A standard signed JWT (a JWS) is Base64URL-encoded , not encrypted. The signature proves the token has not been tampered with and was issued by someone holding the key, but the header and payload are fully readable by anyone who has the token. If you need the payload itself to be confidential, you need a JWE (encrypted JWT), which is a different format. This tool produces signed JWS tokens, the kind used for the vast majority of authentication and authorization flows.
**Q: Why is my RS256 or ES256 signing failing?**
A: The most common causes are: (1) the key is not in PKCS8 format — convert a traditional -----BEGIN RSA PRIVATE KEY----- (PKCS1) key with openssl pkcs8 -topk8 -nocrypt -in old.pem -out pkcs8.pem; (2) the curve does not match the algorithm — ES256 needs a P-256 key, ES384 needs P-384, ES512 needs P-521; (3) you pasted a public key or a certificate instead of the private key; or (4) the key is encrypted with a passphrase, which the Web Crypto API cannot import directly. Decrypt it first with openssl pkey and paste the unencrypted PKCS8 block.
**Q: Does this tool support the alg:none unsigned token?**
A: No, and deliberately so. An alg:none token has no signature, which means anyone can forge one — it is the root of a classic JWT authentication-bypass vulnerability. Because the entire point of an encoder is to produce a signed token, this tool only offers real signing algorithms. If you are studying alg:none for security research, you can construct one by hand by Base64URL-encoding the header and payload and leaving the signature segment empty — the token still ends with a trailing dot (header.payload.) — but you should never accept such a token in production.
**Q: Can I generate a JWT in code instead?**
A: Yes. In Node.js : jsonwebtoken.sign(payload, secret, { algorithm: 'HS256', expiresIn: '1h' }). In Python : jwt.encode(payload, key, algorithm='RS256') with PyJWT. In Go : jwt.NewWithClaims(jwt.SigningMethodES256, claims).SignedString(privateKey). This tool is the fastest way to produce a token for a quick test, a curl request, or a fixture — but in application code you should generate tokens server-side with a maintained library and a key loaded from your secrets manager, never hard-coded.
---
### Free JWT Secret Generator — HS256/384/512
URL: https://go-tools.org/tools/jwt-secret-generator
Generate a strong, RFC-correct JWT secret for HS256/384/512 — 100% in your browser, never sent to a server. base64url, base64 or hex; copy for .env.
#### What is a JWT secret generator?
A JWT secret generator produces the random signing key that an HMAC-signed JSON Web Token uses to prove it has not been tampered with. When you sign a token with HS256, HS384, or HS512, the algorithm runs HMAC over the token's header and payload using a single shared secret; the verifier recomputes the same HMAC with the same secret and accepts the token only if the signatures match. The whole security of that scheme rests on the secret being long and unpredictable — which is exactly what this tool creates: a high-entropy random string, generated in your browser, sized correctly for the algorithm you pick.
It is worth being precise about what this tool does and does not do. It generates the secret key — the value you put in your JWT_SECRET environment variable — not a finished token. If you want to assemble a header and payload and sign them into an actual JWT, that is the job of the JWT Encoder ; to take an existing token apart and verify its signature, use the JWT Decoder . Think of the secret as the key and the encoder as the lock it operates: you generate the key once, store it safely, and reuse it to sign and verify many tokens.
How long should the key be? The answer is fixed by the spec, not by preference. RFC 7518 §3.2 — the JSON Web Algorithms standard — requires that an HMAC key be at least as large as the hash output: "A key of the same size as the hash output (for instance, 256 bits for HS256) or larger MUST be used." That gives a clean table the generator follows automatically:
| Algorithm | HMAC | Min bytes | Min bits | hex chars | base64 chars | base64url chars |
|-----------|------|-----------|----------|-----------|--------------|-----------------|
| HS256 | HMAC-SHA-256 | 32 | 256 | 64 | 44 | 43 |
| HS384 | HMAC-SHA-384 | 48 | 384 | 96 | 64 | 64 |
| HS512 | HMAC-SHA-512 | 64 | 512 | 128 | 88 | 86 |
The character counts come from the RFC 4648 encodings of those byte lengths: hex doubles the byte count; base64 expands by 4⁄3 with padding; base64url drops the padding, so a 32-byte key is 43 base64url characters rather than 44. base64url is JWT's native encoding — URL-safe alphabet, no padding — which is why it is the default output here; a secret in base64url can sit in a header, a URL, or a config value with no escaping.
Randomness is the part you cannot compromise on. This generator draws its bytes from crypto.getRandomValues, the browser's cryptographically secure pseudo-random number generator, the same primitive that backs Web Crypto key generation. It never uses Math.random, which is fast but predictable and completely unsuitable for a signing key — a predictable RNG means a guessable secret, and a guessable secret means forgeable tokens. Because HMAC verification happens locally with the shared secret, an attacker who captures a token can brute-force a weak key offline with no rate limit; tools such as hashcat (mode 16500) and jwt_tool exist precisely to do this. A full-entropy 32-byte random key, on the other hand, is computationally out of reach. The lesson is blunt: never use a password, a dictionary word, or a hand-typed string as a JWT secret — generate a random one.
Finally, generating the key client-side is itself a security property. A signing secret should never be transmitted to a third party, not even the site that helps you create it. Every byte here is produced and encoded in your browser; nothing is uploaded, logged, or stored. When you are ready to ship the key, the Copy for .env button hands you a JWT_SECRET=… line, and if you need it folded into a larger configuration the JSON to .env converter can help. Generate, copy, store it in a secrets manager — and rotate it with a kid header and overlapping validity windows when the time comes.
```
// The secret you generate here goes straight into your signing code.
// Node.js with jsonwebtoken — the JWT_SECRET env var holds the key.
import jwt from 'jsonwebtoken';
const secret = process.env.JWT_SECRET; // e.g. base64url value from this tool
// Sign a token with HS256 (HMAC-SHA-256).
const token = jwt.sign({ sub: 'user-42', role: 'member' }, secret, {
algorithm: 'HS256',
expiresIn: '15m'
});
// Verify it — pin the algorithm to a whitelist; never trust the token's alg.
const payload = jwt.verify(token, secret, { algorithms: ['HS256'] });
// ---------------------------------------------------------------
// Python with PyJWT — same secret, same algorithm pinning.
// import jwt
// token = jwt.encode({"sub": "user-42"}, key, algorithm="HS256")
// payload = jwt.decode(token, key, algorithms=["HS256"]) # whitelist!
// ---------------------------------------------------------------
// Equivalent-strength CLI generation (32 bytes for HS256):
// openssl rand -base64 32
// node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
// python -c "import secrets; print(secrets.token_urlsafe(32))"
```
#### FAQ
**Q: Is my generated JWT secret sent to your server?**
A: No. The secret is generated entirely in your browser with crypto.getRandomValues, the platform's cryptographically secure random number generator. The bytes are produced on your device, encoded locally, and shown only to you. Nothing is uploaded, nothing is logged, nothing is written to disk, and nothing is sent to any third party — open DevTools → Network and you will see zero requests fire when you click Regenerate or Copy. That means a key you generate here is yours alone the instant it appears; no server ever observes it. This is the whole point of generating a signing secret client-side rather than on a website that could, in principle, keep a copy of every key it hands out.
**Q: How do I generate a secure JWT secret?**
A: Three steps. First, pick the signing algorithm your application uses — HS256, HS384, or HS512 — and the generator immediately sizes the key to the RFC 7518 §3.2 minimum for that variant (32, 48, or 64 bytes), so you never hand-pick a number or risk an undersized key. Second, leave the encoding on base64url (JWT's native, URL-safe format) or switch to base64 or hex if your config loader expects one of those. Third, click Copy — or Copy for .env to get a ready-to-paste JWT_SECRET=… line — and store the value in a secrets manager or environment variable, never in source control. Because every byte comes from crypto.getRandomValues, a 32-byte key already carries 256 bits of entropy, which is far beyond the reach of the offline brute-force attacks that break human-chosen secrets. Prefer the command line? The tool also prints equivalent openssl, Node, and Python one-liners you can paste into a terminal.
**Q: How long should an HS256 JWT secret be?**
A: At least 32 bytes (256 bits). RFC 7518 §3.2 — the JSON Web Algorithms spec — states that for HMAC signatures "a key of the same size as the hash output (for instance, 256 bits for HS256) or larger MUST be used." HS256 signs with HMAC-SHA-256 (256-bit hash), so the minimum key is 32 bytes; HS384 uses HMAC-SHA-384 and needs at least 48 bytes (384 bits); HS512 uses HMAC-SHA-512 and needs at least 64 bytes (512 bits). This generator picks the correct minimum automatically when you choose the algorithm, and you can request a longer key. A shorter key is not just weaker — it violates the spec and some libraries will refuse to sign with it.
**Q: What is the difference between base64url, base64, and hex, and which should I pick?**
A: All three encode the same random bytes; they differ only in the character alphabet, not in entropy. base64url is JWT's native encoding — it uses a URL-safe alphabet (- and _ instead of + and /) and omits padding, so it never needs escaping in a token, a URL, or a header. That is why it is the default here. Standard base64 uses +, /, and = padding; choose it when a config loader or library specifically expects classic base64. hex (base16) writes each byte as two characters 0–f, producing a longer but unambiguous string that is handy when a system rejects non-alphanumeric characters. For a JWT_SECRET environment variable, base64url is the safest default; any of the three works as long as you store the exact string and feed it back to your signing library unchanged.
**Q: Can a weak JWT secret be cracked?**
A: Yes — and this is the single biggest risk with HMAC-signed JWTs. Because HS256/384/512 use one shared secret, anyone who holds a token can run an offline brute-force or dictionary attack against the signature with no rate limit and no server contact. Tools like hashcat (mode 16500 targets JWT) and jwt_tool are purpose-built for this; on commodity GPUs a low-entropy secret can typically fall in seconds to hours, and a dictionary word or a leaked password can fall almost immediately. A full-entropy 32-byte random key from this generator is far beyond the reach of brute force. Once an attacker recovers the secret they can forge any token — including one that claims admin privileges — so secret strength is not optional. Generate the signing key with a CSPRNG, never a human-chosen string. For a deeper treatment of weak-secret, alg-confusion, and token-replay attacks, see our guide to JWT security best practices .
**Q: Can I use a password as my JWT secret?**
A: You can, but you should not. A human-memorable password — even a long passphrase — carries far less entropy than 32 random bytes, which makes it a realistic target for the offline brute-force and dictionary attacks described above. JWT secrets are machine-to-machine credentials; nobody needs to memorize them, so there is no reason to trade entropy for memorability. Generate a random secret here, store it in a secrets manager or an environment variable, and let your application read it. If you need memorable credentials for a different purpose, that is a job for a Password Generator or, for storing user passwords, a one-way hash from the bcrypt Generator — not for a token-signing key.
**Q: How do I rotate a JWT secret without breaking live tokens?**
A: Rotate with overlap rather than a hard cutover. Add a key identifier (the kid header) to the tokens you sign so a verifier knows which secret to check against, and publish your active keys — typically through a JWKS endpoint your services read. To rotate: generate a new secret here, start signing new tokens with the new kid while still accepting the previous key for verification, and only retire the old key after every token signed with it has expired. That overlap window means no valid session is invalidated mid-flight. In a suspected compromise, skip the graceful overlap: rotate immediately, drop the old key from the accepted set, and force re-authentication so leaked tokens stop verifying at once.
**Q: How is an HMAC (HS*) key different from an RSA or ECDSA (RS*/ES*) key?**
A: They solve the same problem with opposite key models. HS256/384/512 are HMAC algorithms: they use one symmetric secret — the kind this tool generates — that both signs and verifies, so every party that can verify a token can also forge one. That is simple and fast and ideal when a single service both issues and checks its own tokens. RS* (RSA) and ES* (ECDSA) are asymmetric: they use a key pair, where a private key signs and a separate public key only verifies. You hand the public key to anyone who needs to validate tokens without ever exposing the signing key — the right choice when an identity provider issues tokens that many independent services verify. This generator produces HMAC symmetric secrets only — for the asymmetric side, our RSA key pair generator creates RSA, ECDSA and Ed25519 pairs in the browser. To assemble and sign an actual token with the key you generate, use the JWT Encoder ; to inspect and verify one, use the JWT Decoder .
---
### Length Unit Converter — Metric, Imperial & More
URL: https://go-tools.org/tools/length-converter
Convert between 16 length units instantly — metric, imperial, nautical & astronomical. 1 inch = 2.54 cm. Free, private, runs in your browser.
#### What Is a Length Unit Converter?
A length unit converter is a tool that translates distance and length measurements between different units of measure — bridging the metric system (meter, kilometer, centimeter) and the imperial system (inch, foot, yard, mile), as well as specialized units used in science, navigation, and astronomy.
The metric system, used by most of the world, is decimal-based: each unit differs from the next by a power of 10. The core unit is the meter, officially defined since 2019 by the International Bureau of Weights and Measures (BIPM) in the International System of Units (SI) as the distance light travels in a vacuum in exactly 1/299,792,458 of a second — anchoring the meter to a universal physical constant rather than any physical artifact. Prefixes like kilo- (1,000), centi- (0.01), milli- (0.001), micro- (0.000001), and nano- (0.000000001) scale the meter up or down. The imperial system, still common in the United States, uses inches, feet (12 inches), yards (3 feet), and miles (5,280 feet).
Beyond everyday units, this converter supports nautical miles (used in aviation and maritime navigation), and astronomical units like the light year (9.461 trillion km), the astronomical unit (Earth-Sun distance, ~149.6 million km), and the parsec (~3.26 light years). It also includes the fathom (6 feet, used for water depth) and the furlong (220 yards, used in horse racing).
All conversions use the internationally defined exact factors established by the 1959 international yard and pound agreement, as published by BIPM and NIST, ensuring precise results traceable to the SI definition of the meter. Processing runs entirely in your browser — no data is transmitted to any server, so your measurements stay completely private.
Need to convert other measurement types? Try our weight converter for mass units, volume converter for liquid measurements, or temperature converter for Celsius, Fahrenheit, and Kelvin.
```
// Key conversion factors (exact):
// 1 inch = 2.54 cm
// 1 foot = 0.3048 m
// 1 yard = 0.9144 m
// 1 mile = 1.609344 km
// JavaScript conversion examples:
const inchesToCm = (inches) => inches * 2.54;
const feetToMeters = (feet) => feet * 0.3048;
const milesToKm = (miles) => miles * 1.609344;
const kmToMiles = (km) => km / 1.609344;
console.log(inchesToCm(12)); // 30.48
console.log(feetToMeters(6)); // 1.8288
console.log(milesToKm(26.2)); // 42.164928
```
#### FAQ
**Q: How many centimeters are in an inch?**
A: There are exactly 2.54 centimeters in one inch. This is not an approximation — it is the exact conversion factor defined by the international yard and pound agreement of 1959. To convert inches to centimeters, multiply by 2.54. To convert centimeters to inches, divide by 2.54. For example, 12 inches = 30.48 cm, and 10 cm = 3.937 inches.
**Q: How many feet are in a meter?**
A: One meter equals approximately 3.28084 feet, or more precisely, 1 meter = 3.2808398950131 feet. Conversely, 1 foot = 0.3048 meters exactly. This means a 6-foot person is about 1.8288 meters tall. To convert meters to feet, multiply by 3.28084. To convert feet to meters, multiply by 0.3048.
**Q: What is the formula to convert inches to cm?**
A: To convert inches to centimeters, multiply the inch value by 2.54. The formula is: cm = inches × 2.54. This is an exact conversion factor defined by international agreement in 1959. For example, 6 inches = 6 × 2.54 = 15.24 cm. To convert back from cm to inches, divide by 2.54: inches = cm ÷ 2.54.
**Q: What is a nautical mile and how does it differ from a regular mile?**
A: A nautical mile equals exactly 1,852 meters (about 1.151 regular miles or 6,076 feet). It was originally defined as one minute of arc along a meridian of the Earth, making it naturally suited for navigation. A regular (statute) mile equals 1,609.344 meters or 5,280 feet. Nautical miles are used in aviation and maritime navigation because they relate directly to degrees of latitude: 60 nautical miles = 1 degree of latitude.
**Q: Which countries still use the imperial system?**
A: Only three countries primarily use the imperial system for everyday measurements: the United States, Myanmar (Burma), and Liberia. The UK uses a mix — road distances are in miles, but most other measurements are metric. Canada officially uses metric but commonly uses feet and inches for personal height and real estate. Most scientific, medical, and international trade contexts use the metric system worldwide.
**Q: How tall is 5'7 in cm?**
A: 5 feet 7 inches equals 170.18 centimeters. To calculate: convert feet to inches (5 × 12 = 60), add remaining inches (60 + 7 = 67 total inches), multiply by 2.54 (67 × 2.54 = 170.18 cm). Common heights: 5'0" = 152.4 cm, 5'5" = 165.1 cm, 5'7" = 170.18 cm, 5'10" = 177.8 cm, 6'0" = 182.88 cm, 6'2" = 187.96 cm.
**Q: What is a light year and how far is it?**
A: A light year is the distance that light travels in one year in a vacuum — approximately 9.461 trillion kilometers (9.461 × 10¹² km) or about 5.879 trillion miles. Despite the name, a light year is a unit of distance, not time. For perspective, light from the Sun takes about 8 minutes to reach Earth (1 AU = 149.6 million km), while the nearest star system, Alpha Centauri, is about 4.37 light years away. Light years are used to express distances between stars and galaxies because using kilometers would require unwieldy numbers.
**Q: How accurate is this length converter?**
A: This converter uses the internationally defined exact conversion factors: 1 inch = 25.4 mm exactly, 1 yard = 0.9144 m exactly, and 1 mile = 1,609.344 m exactly. All calculations use IEEE 754 double-precision floating-point arithmetic, providing at least 15 significant digits of precision. For everyday conversions, the results are more accurate than any physical measurement. The only limitation is the inherent rounding of floating-point numbers at extreme precision, which affects digits beyond the 15th significant figure.
**Q: How do metric prefixes work for length units?**
A: Metric length units are based on the meter, with prefixes indicating powers of 10. Common prefixes from large to small: kilo (km) = 1,000 m, no prefix (m) = 1 m, centi (cm) = 0.01 m, milli (mm) = 0.001 m, micro (um) = 0.000001 m, and nano (nm) = 0.000000001 m. Each step between adjacent common prefixes is a factor of 1,000 (except centi, which is 1/100 of a meter). This decimal-based system makes metric conversions straightforward: moving the decimal point is all that is required.
**Q: Is my data safe when using this length converter?**
A: Yes, completely. All conversions are performed locally in your browser using JavaScript. No data is sent to any server — there are no network requests, no cookies, and no analytics on your input. The conversion logic runs entirely on your device, meaning your values never leave your browser. You can verify this by disconnecting from the internet and using the tool — it works fully offline once the page has loaded.
**Q: How many feet are in a mile?**
A: There are exactly 5,280 feet in one mile. This relationship is defined by the statute mile, the standard mile used in the United States and United Kingdom. Other useful conversions: 1 mile = 1,760 yards = 63,360 inches = 1,609.344 meters = 1.609344 kilometers. The word 'mile' derives from the Latin 'mille passus' meaning one thousand paces.
**Q: What is the SI unit of length?**
A: The meter (m) is the SI (International System of Units) base unit of length. Since 2019, it is defined as the distance light travels in a vacuum in exactly 1/299,792,458 of a second. All other metric length units are derived from the meter using decimal prefixes: 1 kilometer = 1,000 meters, 1 centimeter = 0.01 meters, 1 millimeter = 0.001 meters, 1 micrometer = 0.000001 meters, 1 nanometer = 0.000000001 meters.
**Q: I need to convert my height from feet to centimeters for a medical form — how do I do it?**
A: First, convert your total height to inches: multiply the feet by 12 and add any remaining inches. For example, 5 feet 9 inches = (5 x 12) + 9 = 69 inches. Then multiply by 2.54 to get centimeters: 69 x 2.54 = 175.26 cm. You can enter 69 in this tool with inches as the source unit and centimeters as the target to get the exact result. Common heights for reference: 5'4" = 162.6 cm, 5'7" = 170.2 cm, 5'10" = 177.8 cm, 6'1" = 185.4 cm. Medical forms worldwide use centimeters, so this conversion comes up frequently for travelers and expats.
**Q: I need to convert kilometers to miles for a US road trip — what is the quickest way?**
A: Multiply kilometers by 0.621 for a quick estimate, or use this tool for exact results. For mental math on the road, a handy trick is the Fibonacci approximation: consecutive Fibonacci numbers approximate the km-to-miles ratio. So 8 km is about 5 miles, 13 km is about 8 miles, 21 km is about 13 miles. For speed limits, 100 km/h is about 62 mph, 120 km/h is about 75 mph, and 130 km/h is about 81 mph. Enter any distance in this tool to get the precise conversion instantly.
**Q: I need to figure out if my furniture will fit through a doorway — how do I convert between inches and centimeters?**
A: Multiply inches by 2.54 to get centimeters, or divide centimeters by 2.54 to get inches. A standard US interior doorway is 80 inches (203 cm) tall and 30-36 inches (76-91 cm) wide. If your furniture is measured in centimeters (common for IKEA and European brands), divide by 2.54 to compare with your doorway in inches. For example, a 200 cm tall bookshelf is 78.7 inches — it will fit through a standard door with about 1.3 inches to spare. Always measure the diagonal of the item if you need to tilt it to fit through.
---
### Lorem Ipsum Generator — Free Placeholder Text Tool
URL: https://go-tools.org/tools/lorem-ipsum
Generate Lorem Ipsum placeholder text instantly — by paragraph, sentence, word, byte, or list. Copy or download as plain text, HTML, Markdown, or JSON. 100% free, private, in-browser. No sign-up.
#### What Is Lorem Ipsum?
Lorem Ipsum is the placeholder text the design and publishing world reaches for whenever a layout needs words before the real copy exists. It is deliberately meaningless — a scramble of Latin-looking fragments — so that anyone reviewing the work judges the visual design, the typography, and the spacing instead of getting pulled into reading and editing the message. A block of Lorem Ipsum wraps, breaks, and sets type much like genuine prose because it has a natural-feeling distribution of word and sentence lengths, which is exactly why it beats lazy fillers like "asdf asdf" or repeated "text text text" that distort how a real paragraph would flow.
The text is not invented gibberish. It traces back to a passage from Cicero's "De Finibus Bonorum et Malorum" ("On the Ends of Good and Evil"), a work of moral philosophy written in 45 BC. Somewhere along the way the Latin was garbled — words clipped, reordered, and combined — until it stopped being readable Latin and became the neutral, language-agnostic filler we know. The famous opening, "Lorem ipsum dolor sit amet, consectetur adipiscing elit," is itself a corruption: "Lorem" isn't even a real Latin word; it's the tail of "dolorem" (pain) with its first syllable lost.
Lorem Ipsum entered modern practice through print. In the 1960s the Letraset company printed Lorem Ipsum passages on its dry-transfer lettering sheets, giving graphic designers a ready supply of filler to rub onto layouts. It made the leap to the screen in the 1980s when Aldus included it as sample text in PageMaker, the application that launched desktop publishing. From there it became the default in page-layout software, website templates, and design tools, and today every major design app — from InDesign to Figma — can drop Lorem Ipsum into a frame with a single command.
This generator gives you that same filler on demand, but built for how people actually work in 2026: not just paragraphs, but exact word counts, sentence counts, list items, and precise byte budgets, and not just plain text, but HTML, Markdown, and JSON output for pasting straight into markup, documents, fixtures, and mocks. It runs entirely in your browser, so it is instant, private, and available offline. Pair it with the word counter when you need to match a real content slot's length, or the case converter and JSON formatter when you're shaping the filler into a specific format for a template or test. For the full story of where Lorem Ipsum comes from and when not to use it, read our complete guide to Lorem Ipsum .
```
// Generating Lorem Ipsum (simplified)
const WORDS = ['lorem','ipsum','dolor','sit','amet','consectetur','adipiscing','elit','sed','do','eiusmod','tempor'];
const PREFIX = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit';
const pick = (rng) => WORDS[Math.floor(rng() * WORDS.length)];
function sentence(rng, lead) {
const n = 6 + Math.floor(rng() * 9); // 6-14 words
const words = Array.from({ length: n }, () => pick(rng));
const body = words.join(' ');
return lead ? `${lead}, ${body}.` : `${body[0].toUpperCase()}${body.slice(1)}.`;
}
function paragraph(rng, startWithLorem) {
const n = 3 + Math.floor(rng() * 5); // 3-7 sentences
return Array.from({ length: n }, (_, i) =>
sentence(rng, i === 0 && startWithLorem ? PREFIX : undefined)
).join(' ');
}
// Three random paragraphs, beginning with the canonical line
const text = Array.from({ length: 3 }, (_, i) =>
paragraph(Math.random, i === 0)
).join('\n\n');
```
#### FAQ
**Q: What is this Lorem Ipsum generator and what does it do?**
A: It produces Lorem Ipsum — the standard dummy text used as a placeholder in design and development — in whatever quantity and format you need. You choose a unit (paragraphs, sentences, words, list items, or an exact byte count) and an amount, and the tool generates matching filler text instantly. You can output plain text, HTML (with or
wrapping), Markdown, or a JSON array, then copy it to your clipboard or download it as a file. The page opens with three paragraphs already generated so you can grab text immediately. Everything runs in your browser with JavaScript — nothing is uploaded, logged, or stored, and no account is required.
**Q: Is the Lorem Ipsum generator free, and do I need to sign up?**
A: It is completely free with no sign-up, no account, no email capture, and no usage limit. There is no premium tier that gates formats or sizes, and the generated text carries no watermark or attribution requirement — it is placeholder text, free for any use. The tool is funded the same way the rest of the site is and asks nothing of you in return for generating text. You can use it as many times as you like, generate as much text as the size ceilings allow, and download the result without ever creating a profile.
**Q: Does my text get sent to a server, or is it private?**
A: Generation happens 100% client-side in your browser. There is no server round-trip: the JavaScript that builds the text runs locally, which is why output appears instantly even at the largest sizes and why the tool keeps working if your connection drops. Nothing you generate is transmitted, logged, stored, or analyzed. This matters less for placeholder text than for a tool that processes your own input, but it means the generator is fast, reliable, and verifiable — you can open your browser's Network tab and confirm that clicking Regenerate triggers zero network requests.
**Q: What is Lorem Ipsum and where does it come from?**
A: Lorem Ipsum is scrambled, meaningless Latin-like text used to fill space in a layout so designers and clients judge the visual design without being distracted by readable content. It is not random gibberish: it derives from a passage of Cicero's "De Finibus Bonorum et Malorum" ("On the Ends of Good and Evil"), written in 45 BC, with words altered, added, and removed so it no longer reads as real Latin. The text entered design culture in the 1960s when the Letraset company printed Lorem Ipsum passages on its dry-transfer lettering sheets, and it spread worldwide in the 1980s when Aldus bundled it into the PageMaker desktop-publishing software. The canonical opening — "Lorem ipsum dolor sit amet, consectetur adipiscing elit" — is the fragment most people recognize.
**Q: Why use Lorem Ipsum instead of real text or just 'asdf asdf'?**
A: Placeholder text exists so the eye evaluates layout, typography, and spacing rather than reading the words. Real copy pulls reviewers into editing the message instead of the design, and obvious filler like "asdf asdf" or repeated "text text text" produces unnatural word lengths and line breaks that misrepresent how real content will flow. Lorem Ipsum has a roughly normal distribution of word and sentence lengths, so a paragraph of it wraps, hyphenates, and sets type much like genuine prose would. That makes it a faithful stand-in for judging line length, leading, column width, and the rhythm of a text block before the real words are ready.
**Q: How do I generate Lorem Ipsum directly as HTML?**
A: Set the Format control to HTML. Paragraph output is then wrapped in …
tags (one per paragraph), and list output is wrapped in a (note the plural). Fix: match tag names exactly, including case. (2) Unclosed tags — a tag that never has a corresponding close tag or self-closing slash. Fix: add the closing tag or change to . (3) Unescaped special characters — using & directly in text content instead of &, or < instead of <. Fix: replace bare & with & and bare < with < outside CDATA sections. (4) Multiple root elements — XML requires exactly one root element wrapping everything else. Fix: wrap all content in a single root tag. The error message from this tool includes the line and column number of the first problem found.
**Q: Can I use this tool to format XHTML or SVG files?**
A: Yes. XHTML and SVG are both valid XML applications, so this tool formats, minifies, and validates them correctly. For XHTML, it will catch mismatched or unclosed tags that would be silently ignored in HTML5 parsers but are errors in strict XHTML. For SVG, it is particularly useful for formatting complex path-heavy files generated by tools like Figma or Illustrator, making it easier to inspect or edit the element structure manually.
**Q: How does this tool handle XML namespaces?**
A: XML namespaces (xmlns declarations, namespace prefixes like soap:, xsi:, and so on) are fully preserved by the formatter. The namespace declarations remain on the element where they were originally declared and are not moved or deduplicated. Namespace-prefixed element names and attribute names are treated as opaque strings by the formatter — the prefix and local name are preserved exactly as written. The SOAP Envelope example above demonstrates a document with three namespace prefixes.
**Q: Is there a file size limit for XML input?**
A: There is no hard size limit enforced by the tool, but the browser's DOM-based parser will consume memory proportional to the document size. For most real-world XML files (configuration files, API responses, RSS feeds, SOAP payloads) well under 1MB, performance is instant. For very large XML files — multi-megabyte data exports or log files — consider using a command-line tool instead: xmllint --format input.xml on Linux/macOS (part of libxml2), or python3 -c "import xml.dom.minidom; print(xml.dom.minidom.parse('input.xml').toprettyxml(indent=' '))" as a cross-platform option.
**Q: How do I convert XML to JSON or JSON to XML?**
A: This tool focuses on formatting and validating XML structure. To convert between XML and JSON, use the companion tools: XML to JSON Converter converts XML documents to their JSON representation, and JSON to XML Converter converts JSON objects to XML. Both tools are also 100% browser-based with no data upload.
---
### XML to JSON Converter
URL: https://go-tools.org/tools/xml-to-json
Paste XML, get JSON instantly. Converts attributes to @_ keys, handles repeated elements as arrays. 100% in-browser, nothing uploaded, no signup.
#### What is XML-to-JSON Conversion and How Does It Work?
XML (Extensible Markup Language) and JSON (JavaScript Object Notation) are both structured data formats, but they have fundamentally different models: XML is a tree of elements with attributes and mixed content (text interleaved with child elements); JSON is a tree of objects, arrays, strings, numbers, booleans, and null values. Converting between them requires a set of conventions to bridge the mismatch.
This tool uses the most widely adopted convention, the same one used by popular libraries like fast-xml-parser (Node.js), xmltodict (Python), and JAXB (Java):
**1. Attributes → @_ prefix.** XML attributes have no direct JSON equivalent. The convention is to represent them as keys prefixed with @_. So becomes { "@_id": "42", "@_role": "admin" } inside the user object. This prefix is unambiguous: no valid XML element name starts with @, so there is no collision with child element names.
**2. Element text content with attributes → #text.** When an element has both attributes and text content — 29.99 — the text must share the same JSON object as the attributes. The convention is to store it under the key #text, producing { "@_currency": "USD", "#text": "29.99" }. Elements with only text content and no attributes convert to a plain string value.
**3. Repeated sibling elements → arrays.** XML allows multiple child elements with the same name; JSON objects cannot have duplicate keys. The solution is to collect same-named siblings into an array. One - child becomes a single object; two or more
- children become an array of objects. This is the most important behavioral detail to understand: the JSON shape changes based on how many siblings exist in the XML.
**4. No type coercion — all values stay strings.** XML has no native type system for text content. A value of "123" in XML is a string. Converting it to the JSON number 123 requires making an assumption about the author's intent — an assumption that is wrong for ZIP codes ("01234" → 1234), phone numbers, padded identifiers, and precision-sensitive decimal strings. This tool preserves all values as strings. Apply type coercion in your own code for the fields where you know the type.
**5. Lossy for comments, processing instructions, and namespaces.** XML supports features that JSON does not: comments (), processing instructions (), and namespace semantics. These are discarded or approximated during conversion. For lossless XML work — reformatting, minifying, validating — use the
XML Formatter instead. For the reverse conversion — building XML from JSON — use the JSON to XML Converter .
**Why convert XML to JSON at all?** JSON is the native format of JavaScript and the default interchange format for REST APIs. If you receive XML from a legacy SOAP service, an RSS feed, a sitemap, or an enterprise system, converting it to JSON lets you work with the data using standard JavaScript object access, JSON path queries, and any JSON-aware database or API. The conversion is a one-way bridge: useful for consuming XML data in a modern stack, but not for preserving or round-tripping XML documents.
```
// Convert XML to JSON in Node.js using fast-xml-parser
import { XMLParser } from 'fast-xml-parser';
const xml = `
Wireless Headphones
79.99
`;
const parser = new XMLParser({
ignoreAttributes: false, // preserve attributes
attributeNamePrefix: '@_', // @_ prefix for attributes
textNodeName: '#text', // #text for mixed element content
parseAttributeValue: false, // no type coercion on attributes
parseTagValue: false, // no type coercion on element text
});
const result = parser.parse(xml);
console.log(JSON.stringify(result, null, 2));
// {
// "catalog": {
// "product": {
// "@_id": "P01",
// "name": "Wireless Headphones",
// "price": {
// "@_currency": "USD",
// "#text": "79.99"
// }
// }
// }
// }
```
#### FAQ
**Q: Is my XML data sent to a server when I use this tool?**
A: No. All conversion happens entirely inside your browser using JavaScript. Your XML is never transmitted over the network, never stored on any server, and never logged or analyzed. This makes the tool safe to use with XML payloads containing API credentials, internal service configuration, SOAP WS-Security tokens, healthcare HL7/FHIR data, or any other sensitive content. You can verify this by opening your browser's Network tab — you will see zero requests triggered by pasting or converting XML.
**Q: How do XML attributes map in the JSON output?**
A: XML attributes become JSON keys prefixed with @_. For example, produces a JSON object containing "@_id": "P01" and "@_category": "electronics" alongside any child element keys. When an element has both attributes and text content — such as 29.99 — the text content is stored under the special key "#text", so the result is { "@_currency": "USD", "#text": "29.99" }. This convention is consistent and predictable: @_ always means attribute, #text always means element text content.
**Q: Does the converter coerce numbers or booleans?**
A: No. All XML text content and attribute values become JSON strings, regardless of how they look. 42 becomes "count": "42", not 42. true becomes "enabled": "true", not true. This is intentional and important: it preserves leading zeros (phone numbers, account codes, ZIP codes like "01234"), numeric precision for values like "0.100", and string values that happen to look boolean. If you need numbers or booleans in your downstream JSON, apply type coercion in your own code after converting — where you control exactly which fields get coerced.
**Q: How are repeated (same-named sibling) elements handled?**
A: A single child element becomes a JSON object. Two or more child elements with the same tag name under the same parent become a JSON array. For example, - a
produces { "root": { "item": "a" } } — item is an object (string). But - a
- b
produces { "root": { "item": ["a", "b"] } } — item is an array. This means the structure of your JSON output depends on the number of sibling elements in the XML, which is one reason XML-to-JSON conversion is convention-based. If your XML schema can have either one or many items, your consumer code must handle both the object and array cases.
**Q: Is XML-to-JSON conversion lossless?**
A: No. XML has features that have no JSON equivalent and are dropped during conversion: XML comments () are discarded, processing instructions () are discarded, namespace prefix bindings are partially preserved as @_ attributes but their semantics are not, and the relative order of mixed-content nodes (text interleaved with child elements) may not round-trip perfectly. For purely structural XML without comments or processing instructions, the conversion preserves all element names, attribute names, attribute values, and text content. For lossless XML work — formatting, validating, or inspecting XML without any data loss — use the XML Formatter instead.
**Q: How do I convert JSON back to XML?**
A: Use our companion JSON to XML Converter . It applies the same conventions in reverse: @_-prefixed keys become XML attributes, #text keys become element text content, and JSON arrays become repeated same-named sibling elements. This makes the two tools symmetric for round-trip use cases.
**Q: What happens to XML namespaces?**
A: Namespace declarations (xmlns="..." and xmlns:prefix="...") are treated as regular attributes and appear in the JSON output as @_xmlns and @_xmlns:prefix keys. The namespace prefix in element names is preserved as part of the element name key (e.g., becomes "soap:Body" in the JSON). The semantic meaning of namespaces — that two prefixes might point to the same URI — is not interpreted. If precise namespace handling matters for your use case, parse the XML in a namespace-aware parser rather than converting to JSON.
**Q: Why does 0123 become "0123" and not 123?**
A: Because the converter performs no type coercion. The string "0123" and the number 123 are different values: "0123" has a leading zero that is meaningful in many contexts (account codes, postal codes, national identification numbers, padded identifiers). Silently dropping that leading zero would corrupt data. The safe default is to preserve all values as strings exactly as they appear in the XML. Apply numeric parsing selectively in your own code for the specific fields where you know the value is always a plain integer.
**Q: What is the difference between this tool and an XML formatter?**
A: The XML Formatter reformats XML — it changes indentation and whitespace but the output is still XML. This XML-to-JSON Converter changes the format entirely: the output is a JSON document that represents the XML structure using the @_ attribute convention. Use the formatter when you want to read, edit, validate, or minify XML. Use this converter when you need to work with the XML data in a JavaScript application, feed it into a REST API, or store it in a JSON document store.
**Q: Is there a file size limit?**
A: There is no hard limit, but inputs larger than 200KB automatically switch from live conversion to manual mode. In manual mode, a Convert button appears and conversion runs only when you click it — this keeps the browser responsive while parsing large XML documents. For very large XML files (multi-megabyte data exports), consider command-line tools for better performance: python3 -c "import sys, xmltodict, json; print(json.dumps(xmltodict.parse(sys.stdin.read()), indent=2))" or node -e with a dedicated XML-to-JSON library.
**Q: Does the converter handle CDATA sections?**
A: Yes. CDATA section content () is treated as element text content and appears as a plain string value in the JSON output. The CDATA delimiters themselves are stripped — only the content inside is preserved. For example, produces "note": "if (a < b) return;" in JSON. This is the correct behavior: CDATA is just a way to embed text with special characters without escaping them; the semantic meaning is the text content.
**Q: Can I convert XML with multiple root elements?**
A: No. XML with multiple root elements is not well-formed, and this tool requires well-formed XML input. If your XML parser gives you multiple root elements (common when stitching together XML fragments), wrap them in a single root element before converting. For example, if you have , convert it as . The error message will indicate the position of the well-formedness problem so you can fix it quickly.
---
### YAML to JSON Converter
URL: https://go-tools.org/tools/yaml-to-json
Paste YAML, get JSON instantly. Live conversion in your browser. K8s manifests, OpenAPI specs, helm values supported. 100% private, no upload.
#### What is JSON and Why Convert from YAML?
JSON (JavaScript Object Notation) is a lightweight, text-based data interchange format standardized as RFC 8259 and ECMA-404. It supports six data types — strings, numbers, booleans, null, arrays, and objects — with a strict, minimal syntax that virtually every programming language, API, and toolchain can parse natively. While YAML is the preferred format for human-written configuration files (Kubernetes manifests, GitHub Actions, Ansible playbooks, Helm values), JSON is the universal machine-readable format for APIs, automation scripts, and programmatic data processing.
Converting YAML to JSON is therefore one of the most common tasks in DevOps and backend development — you have a YAML config file but need JSON to feed into a REST API, query with jq, or process with JavaScript tooling.
This tool has four important differentiators compared to typical online converters:
**1. Multi-Document YAML Handling.** YAML supports multiple documents in a single stream separated by --- (the document start marker). Many real-world YAML files — including some Kubernetes manifests and Ansible playbooks — contain multiple documents. This tool uses parseAllDocuments from the eemeli/yaml library with { version: '1.2', merge: true } options and returns the first document as JSON, clearly communicating what was taken. If you need all documents, split on --- and convert each individually.
**2. Anchor and Alias Expansion.** YAML anchors (&name) and aliases (*name) allow reuse of data blocks — a powerful YAML feature with no JSON equivalent. This tool fully expands all anchors and aliases (including merge keys: <<: *anchor) so the JSON output contains complete, self-contained data without any references. This is always the correct transformation because JSON has no reference syntax. The expansion is handled safely by the eemeli/yaml library, which includes protection against circular references. Learn how this compares to the reverse direction at JSON to YAML Converter .
**3. Comment Loss — Educational Transparency.** YAML supports # comments, which are frequently used in Kubernetes manifests, Helm values, and Ansible playbooks to document intent. JSON has no comment syntax, so comments are permanently dropped during conversion. This is not a bug — it is a fundamental format difference. This tool makes this explicit so you know what to expect. If you need to preserve annotations, encode them as JSON fields (_comment keys or a dedicated metadata object) before converting, or keep YAML as the authoritative source. See our deep dive on YAML-JSON differences for more on format tradeoffs.
**4. 100% Browser-Based Privacy.** Your YAML data — which often contains Kubernetes secrets, database credentials, Helm values with passwords, and internal service configurations — never leaves your browser. No data is sent to any server. You can verify this in your browser's Network tab. After converting to JSON, you can validate and format the result with our JSON Formatter before using it downstream.
YAML's richness (comments, anchors, multi-document support, block scalars) makes it excellent for human-authored configuration files where readability and documentation matter. JSON's strictness and universality make it the better choice when a machine is the primary consumer. This converter bridges the two worlds: keep your configuration in YAML for human maintainability, convert to JSON when you need machine-readable interchange. Need to compare two JSON documents and find what changed? Try our JSON Diff .
```
// Convert YAML to JSON in Node.js using the eemeli/yaml library
import { parseAllDocuments } from 'yaml';
const yamlString = `apiVersion: apps/v1
kind: Deployment`;
// parseAllDocuments handles multi-document YAML (--- separator)
// version: '1.2' ensures yes/no are strings, not booleans
// merge: true expands anchor/alias merge keys (<<: *anchor)
const docs = parseAllDocuments(yamlString, { version: '1.2', merge: true });
// Take the first document (skip additional --- blocks)
const json = JSON.stringify(docs[0].toJSON(), null, 2);
console.log(json);
// {
// "apiVersion": "apps/v1",
// "kind": "Deployment"
// }
```
#### FAQ
**Q: How do I convert YAML to JSON online?**
A: Paste your YAML into the input field above. The tool converts it to JSON instantly in your browser — no button click needed. You can adjust the output indentation (2 or 4 spaces) from the Options panel. Once the JSON appears in the output area, click Copy to grab it to your clipboard or Download to save it as a .json file. Everything runs locally — your data never leaves your device.
**Q: How does this tool handle multi-document YAML (--- separator)?**
A: YAML supports multiple documents in a single stream, separated by --- (the document start marker). When you paste a multi-document YAML string, this tool uses parseAllDocuments from the eemeli/yaml library and returns the first document as JSON. The additional documents beyond the first are silently ignored. If you need to process all documents, split your YAML on --- and convert each section individually. The tool shows the first document's JSON so you can verify the result.
**Q: How are YAML anchors and aliases (&anchor and *alias) handled?**
A: YAML anchors (&name) define a reusable block, and aliases (*name) reference it. This tool fully expands all anchors and aliases during parsing, so the output JSON contains the complete, dereferenced data. For example, if a YAML anchor defines a set of resource limits and multiple services alias it with merge keys (<<: *anchor), the JSON output shows every field explicitly inlined for each service. This is the correct behavior for JSON, which has no concept of references. The eemeli/yaml library handles anchor/alias expansion safely, including circular reference detection.
**Q: Are YAML comments preserved in the JSON output?**
A: No. JSON does not support comments of any kind — no #, //, or /* */ syntax. When you convert YAML to JSON, all comments are permanently lost. This is a fundamental format difference, not a limitation of this tool. If you need to preserve annotations, consider encoding them as a dedicated JSON field (such as a _comment key) before converting, or keeping the YAML source as the authoritative version with comments. This tool clearly reflects the data as JSON without any comment approximation, which is the correct and standard behavior.
**Q: How do I use this tool with a Kubernetes manifest?**
A: Paste your Kubernetes YAML manifest (from a .yaml file, kubectl get -o yaml output, or a Helm template) into the input field. The JSON output can then be queried with jq, sent directly to the Kubernetes REST API, used in Terraform data sources, or processed by any tooling that expects JSON. A common workflow is to convert YAML manifests to JSON to extract specific fields — for example: jq '.spec.replicas' on the JSON output to verify replica counts across deployments. The K8s Deployment example above shows a complete manifest you can load and modify.
**Q: How does this tool help with Docker Compose files?**
A: Docker Compose files are YAML by convention. Converting them to JSON lets you process service definitions with JavaScript tooling, jq scripts, or any system that reads JSON. Common use cases include extracting all image names to build a dependency list, generating reports from a compose file, or feeding Compose configurations into CI/CD orchestration tools that accept JSON. Paste your compose.yaml into the input and the JSON output is immediately ready for downstream processing.
**Q: What is the difference between YAML 1.1 and YAML 1.2, and which does this tool use?**
A: YAML 1.1 (the older spec, still used by PyYAML, Ansible, Ruby Psych, and many Kubernetes tools) treats bare strings like yes, no, on, off, y, and n as boolean true/false values. This caused the infamous Norway Problem where the ISO country code 'NO' was parsed as false. YAML 1.2 (the current spec, released in 2009) fixed this: all bare strings are strings, and only true/false are boolean. This tool uses the YAML 1.2 schema for parsing, meaning yes and no in your YAML input are preserved as the string values 'yes' and 'no' in the JSON output — not boolean true and false. This is the correct, modern behavior. If your YAML was originally authored for a YAML 1.1 parser and relied on yes/no as booleans, be aware the JSON output will treat them as strings.
**Q: Why does YAML forbid tab indentation?**
A: The YAML specification explicitly forbids tab characters (\t) for indentation — only spaces are allowed. This is a deliberate design decision to avoid the ambiguity caused by inconsistent tab width across editors. If your YAML uses tabs for indentation (common when copying from text editors that auto-convert spaces to tabs), the YAML parser will throw a parse error. The fix is to replace all tab indentation with spaces. Most code editors have a setting to convert tabs to spaces (for example, 'Expand Tabs' in Vim, 'Insert Spaces' in VS Code). If you paste YAML and see a parse error mentioning 'tab' or 'indentation', this is almost always the cause.
**Q: Can large numbers lose precision when converting YAML to JSON?**
A: Yes. This is a fundamental JavaScript limitation that affects all browser-based tools. JavaScript's IEEE 754 double-precision float can only represent integers exactly up to 2^53 - 1 (9007199254740991). YAML numbers larger than this — such as Kubernetes int64 fields like resourceVersion — will be silently rounded when the YAML parser hands them to JavaScript's number type. For example, the YAML value 9007199254740993 becomes 9007199254740992 in the JSON output. The safe workaround is to quote large numbers in your YAML source (resourceVersion: '9007199254740993') so the parser treats them as strings, which are then preserved exactly in JSON as string values.
**Q: How can I convert YAML to JSON on the command line?**
A: The most popular approach uses yq (Mike Farah's version) and jq. Install yq: brew install yq on macOS or download from github.com/mikefarah/yq/releases for Linux. Then run: yq -o json input.yaml to convert a YAML file to JSON, or cat input.yaml | yq -o json - to pipe from stdin. For pretty-printed output: yq -o json input.yaml | jq . — this pipes the JSON through jq for consistent formatting. For a Python one-liner: python3 -c "import sys, json, yaml; print(json.dumps(yaml.safe_load(sys.stdin), indent=2))" < input.yaml. For multi-document YAML with yq: yq -o json '.[0]' input.yaml to extract only the first document as JSON.
**Q: Is my YAML data sent to any server when I use this tool?**
A: No. All conversion happens entirely in your browser using JavaScript. Your YAML data is never transmitted over the network, never stored on any server, and never logged or analyzed. This makes the tool safe to use with Kubernetes secrets, database credentials, internal Helm values, API keys in config files, and any other sensitive infrastructure configuration. You can verify this by opening your browser's Network tab — you will see zero requests triggered by pasting YAML.
**Q: Is there a file size limit for YAML input?**
A: There is no hard file size limit, but large inputs (over 200KB) automatically switch from live conversion to manual mode. In manual mode, a Convert button appears and conversion runs only when you click it — this prevents the browser's main thread from blocking on every keystroke. For very large YAML files (multi-megabyte), consider using command-line tools like yq for better performance. The tool efficiently handles typical real-world payloads like full Kubernetes namespace exports, large OpenAPI specs, and multi-service Helm chart values files.
## Blog
### AES Decryption Failed: Key, IV, Mode and Padding Fixes
URL: https://go-tools.org/blog/aes-decryption-failed-troubleshooting-guide
AES decryption failed? A wrong key throws a padding error, a wrong IV corrupts only block one, and GCM tags move between languages. Debug yours free online.
# AES Decryption Failed: Key, IV, Mode and Padding Fixes
When AES decryption failed in your logs, the message you got is probably describing the wrong problem. Four unrelated bugs produce nearly identical symptoms, and the most common one, a wrong key, announces itself as a padding error.
Ranked by how often each one turns out to be the culprit, starting from a CBC decryption that throws `BadPaddingException`:
1. The key bytes differ between the two sides. This is by a wide margin the most common cause.
2. The key derivation differs. Same passphrase, different KDF or iteration count, so different key bytes.
3. The ciphertext was damaged in transit: truncated, base64 mangled, or round-tripped through a text encoding.
4. The IV is wrong. This one is real, but it does *not* throw a padding error. It corrupts sixteen bytes and raises nothing.
The ordering is structural. CBC checks padding as the last step of decryption, after the key has been applied and the chain unwound, so padding is a checksum on everything upstream and it fails loudly no matter which upstream thing broke. You can also skip straight to the bisection in section 9 and paste your ciphertext into the [AES decrypt tool](/tools/aes-decrypt).
Everything below was measured on `java 1.8.0_162`, `node v25.8.2` and `openssl 3.6.2`. Defaults move between versions, so treat the version numbers as part of the result.
## 1. Start with what your error actually rules out
An AES failure message says almost nothing about the cause and a lot about what the cause cannot be. That makes it good for deleting branches even though it will never hand you the answer.
| What you see | What it rules out | What is still live |
|---|---|---|
| `BadPaddingException`, `bad decrypt`, `wrong final block length` | GCM; a pure IV mistake; a decode failure | wrong key, wrong KDF, truncated ciphertext, IV bytes eaten as ciphertext, mode mismatch, padding scheme mismatch |
| GCM `Authentication failed`, `Unsupported state or unable to authenticate data` | padding; any theory involving partial output | wrong key, wrong nonce, detached or misplaced tag, wrong tag length, mismatched AAD |
| No exception, output is garbage | every authenticated mode | ECB, CTR, CBC that got lucky, mode mismatch, wrong IV |
### `BadPaddingException`, `bad decrypt`, `wrong final block length`
These three are the same event in three ecosystems: Java, OpenSSL and .NET. It fires at the end of CBC or ECB decryption, when the last plaintext block does not end in a valid PKCS#7 pattern.
The useful part is the negative: getting this far means your base64 or hex decoded and the byte count was a nonzero multiple of 16, so the transport did not shred the data and you are not in GCM. `wrong final block length` is the exception. There the count was *not* a multiple of 16, which points at truncation rather than the key, so jump to section 8.
### GCM `Authentication failed` and friends
GCM compares the tag before releasing a single byte of plaintext, as NIST SP 800-38D requires. That makes it honest in a way the padding error is not: something in the tuple (key, nonce, ciphertext, additional authenticated data, tag) does not match what the encryptor used. It cannot tell you which element, and it never will, because narrowing that down is deliberately outside what the algorithm does. Section 6 covers the element that breaks most often across languages: where the tag sits in the output.
### No error, but the output is garbage
This is the dangerous outcome, because a dashboard records it as success. CTR never throws and ECB never throws. CBC throws only when the final byte pattern fails the padding check, and with a wrong key that byte is effectively random, so roughly one attempt in 256 lands on `0x01` and validates. A little under 0.4% of wrong-key CBC decryptions "succeed". Garbage has a shape, though, and the shape names the bug: sections 4 and 5 have the two fingerprints worth memorising.
## 2. The most misleading error in AES
One measurement reorders most people's debugging priorities. Key `0123456789abcdef`, all-zero IV, `AES/CBC/PKCS5Padding`, plaintext `hello world`, on `java 1.8.0_162` with the JDK's built-in SunJCE provider:
| Scenario | Change | Measured result |
|---|---|---|
| A | Key wrong by 1 byte (last character `f` → `X`) | throws `javax.crypto.BadPaddingException: Given final block not properly padded`. The padding itself was never malformed; the error is misleading |
| B | Key correct, IV wrong by 1 byte | no exception, plaintext `hello world` came back as `iello world`. Only the corresponding byte of the first block was damaged |
| C | Key correct, decrypt the CBC ciphertext with `AES/ECB` | silently succeeded, no exception. A mode mismatch does not have to raise anything |
Scenario A misdirects entire afternoons. Scenario C ships bad data to production.
### Why a wrong key produces a padding error
Nothing about the padding was wrong. The encryptor appended five `0x05` bytes to bring `hello world` up to sixteen, encrypted that block, and it is sitting in your ciphertext unharmed.
The failure happens on the way out. CBC decryption runs the block cipher in reverse, XORs each result with the previous ciphertext block, and only then reads the tail of the final block to decide how many bytes to strip. With the wrong key the cipher produces sixteen bytes of noise, and noise almost never ends in a valid PKCS#7 pattern. The library reports what it saw, bad padding, which is true and useless.
Read `BadPaddingException` as "the plaintext I reconstructed does not end the way padded plaintext ends". The most likely reason your reconstruction is wrong is the key, which is why a search for `aes decrypt wrong key` and a search for a bad padding exception land you in the same threads: the two symptoms are one symptom. One design note while you are in there. Never expose that distinction to a caller, because telling "padding invalid" apart from "padding valid, content wrong" is what a padding oracle attack feeds on (Vaudenay, EUROCRYPT 2002).
### What GCM does differently
GCM inverts the order, verifying the tag before producing any plaintext, so there is no window in which partially correct bytes exist. A GCM failure never leaves you wondering whether the output is real, because there is no output. GCM also has no padding at all, being a counter mode underneath, so ciphertext length equals plaintext length. A padding error in a system you thought was GCM therefore proves the system is not GCM, usually a config that fell back to CBC.
## 3. Are both sides using the same key bytes?
AES does not see your key string. It sees 16, 24 or 32 bytes. Two systems can hold identical key material in a config file and still disagree, because the text matching says nothing about what each side decodes it into.
### The three ways a key string gets turned into bytes
Hand the literal string `0123456789abcdef` to three different libraries:
```
as hex -> 8 bytes (invalid AES key length)
as base64 -> 12 bytes (invalid AES key length)
as raw UTF-8 -> 16 bytes (valid AES-128)
```
Sixteen characters produce three different byte counts. The case is nasty precisely because it is valid under all three readings: every character is in both the hex and base64 alphabets, and sixteen characters is a legal length for both decoders, so nothing errors at parse time.
The [JWT invalid signature troubleshooting guide](/blog/jwt-invalid-signature-troubleshooting-guide) has the full cross-library matrix of how each ecosystem interprets a secret string; the short version for AES is to write down which encoding your key material is in and make both sides decode explicitly. The HMAC form of the same bug bites webhook receivers, covered in the [webhook signature verification guide](/blog/webhook-signature-verification-failed-hmac-guide).
### AES is strict: exactly 16, 24 or 32 bytes
This is where AES differs from the primitive most developers meet first. HMAC accepts any key length: RFC 2104 hashes anything longer than the block size and zero-pads anything shorter, so an [HMAC generator](/tools/hmac-generator) takes a 7-byte or 700-byte secret without complaint. AES has exactly three legal key lengths and rejects everything else before a single block is processed.
The strictness helps, because a length error is the one AES failure that names its own cause instead of hiding behind padding. Our tool phrases it as `Key must be 16, 24, or 32 bytes (AES-128/192/256).` The traps that produce a wrong length:
- A trailing newline from `KEY=$(cat key.txt)` or `echo "$KEY"`. Use `printf` and `echo -n` instead. A trailing space pasted out of a secrets manager UI does the same thing.
- A `0x` prefix copied out of a debugger, which leaves thirty-four characters that are no longer valid hex.
- Non-ASCII characters. `contraseña` is 10 characters and 11 bytes in UTF-8, so a "32-character" passphrase with one accented letter is 33 bytes.
### `SecretKeySpec` and the platform default charset
Java has a version of this that only appears after deployment. `"my secret".getBytes()` with no argument uses the platform default charset, which before JDK 18 came from the `file.encoding` property and therefore from the machine's OS and locale. A laptop on UTF-8 and a container on ANSI_X3.4-1968 produce different bytes for any non-ASCII character. JEP 400 made UTF-8 the default in JDK 18, which fixes new code and nothing else.
```java
// wrong: bytes depend on the machine
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(), "AES");
// right: bytes depend on nothing
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "AES");
```
If your code works locally, fails on the server with a padding error, and the passphrase contains anything outside ASCII, check this first.
## 4. The IV: where it goes and how a wrong one looks
An `aes iv mismatch` is the failure people suspect first and diagnose last, because it does not behave like the others: it is quiet, and its damage is local.
### A wrong IV corrupts exactly one block
Look again at scenario B. Key correct, IV wrong by one byte:
```
hello world -> iello world
```
Nothing threw, and exactly one character changed. Write down the CBC step for the first block and it is obvious: `P1 = D(C1) XOR IV`. The IV is XORed straight into the first plaintext block and touches nothing else, so flipping one bit of the IV flips the same bit of the plaintext in the same position. Here `h` (0x68) became `i` (0x69), so the IV's first byte moved by exactly 0x01.
That gives you a fingerprint. In CBC, first 16 bytes garbage and everything after them clean means the IV is wrong and the key is right. Every block garbage means the key is wrong. That one observation separates the two most common causes without changing a line of code, and the [AES decrypt tool](/tools/aes-decrypt) shows the decoded bytes so you can read it directly.
As for why nothing threw: `hello world` is 11 bytes, so it is a single block, and the PKCS#7 padding lives in bytes 11 through 15 of it. The IV byte that changed was byte 0, so the padding region was untouched and validated. Corrupt an IV byte at position 11 or later and you get a padding error instead, which is another route by which the padding error lies to you.
### Three transmission conventions
No standard says where the IV goes. Instead there are three habits, and they interoperate badly.
Prepending it, as `iv || ciphertext`, is the most common convention and the default in our tools. Both sides must agree on how much to strip: 16 bytes for CBC and CTR, 12 for GCM. The mirror-image bug is a producer that prepends and a consumer that does not. The first 16 bytes of "ciphertext" are then the IV, every block shifts, and you get a padding error.
Giving it a separate field, `{"iv": "...", "ciphertext": "..."}`, is cleaner in principle and doubles the places an encoding can disagree, since the IV now has its own base64-versus-hex question.
The third habit is a fixed constant, usually all zeros, hardcoded because someone needed determinism. It interoperates perfectly, which is what makes it the dangerous one: in CBC a fixed IV leaks equality across records, and in GCM reusing a nonce under one key reveals the XOR of the two plaintexts and can expose the GHASH subkey that authenticates the tag. SP 800-38D is explicit about uniqueness.
The tool's bare-ciphertext switch plus an explicit IV override tests all three conventions against the same bytes in a minute.
### GCM's IV is 12 bytes, not 16
Teams that adopt GCM by editing an existing CBC path carry the 16-byte IV across, and the result fails without a clue.
SP 800-38D standardises a 96-bit IV. Other lengths are permitted but they are not simply "a longer IV": when the IV is not 96 bits, GCM derives its initial counter block by running the IV through GHASH instead of using it directly. The same 16 bytes used as a nonce therefore produce a completely different keystream and tag than the first 12 would, and you get a generic authentication failure. If the ciphertext came from elsewhere and you are guessing at the layout, count backwards: the tag is the last 16 bytes, the nonce almost always the first 12.
## 5. Mode mismatch, including the silent kind
### `Cipher.getInstance("AES")` is ECB
Java lets you name a cipher without naming a mode or a padding scheme. It does not refuse and it does not warn. Under the JDK's built-in SunJCE provider it fills the blanks with ECB and PKCS5Padding.
Proving it needs the right experiment: encrypt 32 identical bytes (two blocks of `A`) with key `0123456789abcdef`, then check whether the two ciphertext blocks match. On `java 1.8.0_162`:
```
getInstance("AES") ciphertext = 3bfd04cc0d7ed55358e2cbe19de213833bfd04cc0d7ed55358e2cbe19de21383377222e061a924c591cd9c27ea163ed4
block1 = 3bfd04cc0d7ed55358e2cbe19de21383
block2 = 3bfd04cc0d7ed55358e2cbe19de21383 <- identical blocks = the ECB fingerprint (plaintext structure leaks)
getInstance("AES/CBC/PKCS5Padding") blocks differ = chaining is active
```
The two blocks match byte for byte. That is the ECB signature, the same property that makes the famous encrypted-penguin image still look like a penguin. The experiment only works with identical plaintext blocks: sixteen `A` bytes followed by sixteen `B` bytes produce two different ciphertext blocks under ECB as well, and you would wrongly conclude the default was CBC.
Scope the result carefully: it describes the JDK's built-in SunJCE provider on the version above. The default transformation is a provider decision, so another provider such as BouncyCastle can resolve the same shorthand differently. What generalises is the weaker claim: an unqualified transformation string means whatever your provider decides, which is the reason never to write one.
### The wrong mode may not raise an error
Scenario C decrypted CBC ciphertext with `AES/ECB` and returned the correct plaintext with no exception. That looks impossible until you write out the arithmetic. CBC encryption of the first block is `C1 = E(P1 XOR IV)`, and ECB decryption of that block is `D(C1) = P1 XOR IV`. The IV here was all zeros, so `P1 XOR 0 = P1` and the first block decrypts perfectly. `hello world` is one block long, so "the first block" was the whole message.
The rule generalises: with a zero IV, ECB and CBC agree on the first block and disagree on every block after it. Decrypt a long CBC message as ECB and you get sixteen clean bytes followed by noise, the exact inverse of the wrong-IV fingerprint. Two opposite shapes point at two different bugs, and neither one produces an error message. Hardcoded zero IVs are common enough that this comes up outside the laboratory.
### What a minimal call gives you in each language
| Ecosystem | Minimal call | Mode you actually get |
|---|---|---|
| Java (SunJCE) | `Cipher.getInstance("AES")` | ECB with PKCS5Padding, silently |
| Node `crypto` | `createDecipheriv('aes-256-cbc', key, iv)` | whatever the algorithm string says; no default exists |
| Web Crypto | `crypto.subtle.decrypt({ name: 'AES-CBC', iv }, ...)` | named explicitly; ECB is not implemented at all |
| Python `cryptography` | `Cipher(algorithms.AES(key), modes.CBC(iv))` | the mode object is mandatory |
| PyCryptodome | `AES.new(key, AES.MODE_ECB)` | mandatory argument, but ECB is right there in the autocomplete |
| Go `crypto/aes` | `aes.NewCipher(key)` returns a raw `cipher.Block` | calling `Decrypt` on that block *is* ECB; wrap it in `cipher.NewCBCDecrypter` or `cipher.NewGCM` |
| CryptoJS | `CryptoJS.AES.decrypt(ct, "passphrase")` | CBC, PKCS#7, EVP_BytesToKey with MD5 (see section 7) |
Ecosystems where the mode lives in a string or an object never surprise you. The two that offer a "just AES" call, Java and Go, are where the accidental-ECB reports come from. If the code does not tell you which mode produced a given ciphertext, [switch modes against the same bytes](/tools/aes-decrypt) until one of them returns something readable.
## 6. GCM: the same bytes, different APIs
Most cross-language `aes gcm auth tag` failures are not cryptographic. Both sides computed the same 16 bytes and disagree about where those bytes live.
### The measurement
Key = 32 bytes `0123456789abcdef0123456789abcdef`, IV = 12 zero bytes, plaintext `hello world`, on `node v25.8.2` and `java 1.8.0_162`:
```
Node ciphertext = a616cd6d7d2328379d41e5 (11 B) <- update+final
authTag = c87af9f8ad7148e873fa797292c0af3f (16 B) <- fetched separately via getAuthTag()
Java doFinal() = a616cd6d7d2328379d41e5c87af9f8ad7148e873fa797292c0af3f (27 B) <- ciphertext and tag already concatenated
```
`Node ciphertext || authTag` is exactly `Java doFinal()`, all 27 bytes of it. There is no encoding difference to negotiate; Node simply hands you the two pieces separately and Java hands them over glued together. The 11 bytes of plaintext also gave 11 bytes of ciphertext, because GCM adds no padding. That is why a padding error can never come from a genuine GCM path.
### Concatenated or separated, by runtime
| Runtime | Encrypt API | Where the tag ends up |
|---|---|---|
| Node `crypto` | `update()` + `final()`, then `getAuthTag()` | separate |
| Java (SunJCE, `AES/GCM/NoPadding`) | `doFinal()` | appended |
| Go `cipher.AEAD` | `Seal()` | appended |
| Python `cryptography`, `AESGCM` | `encrypt()` | appended |
| Python `cryptography`, `Cipher` + `modes.GCM` | `finalize()`, then `encryptor.tag` | separate |
| Web Crypto | `crypto.subtle.encrypt` | appended |
Node is the odd one out among the high-level APIs, which is why Node-to-anything is the most reported direction of failure. When you inherit a blob and cannot tell which convention produced it, [check whether the last 16 bytes are the tag](/tools/aes-decrypt) before you go near the key. Packing Node output for a Java, Go, Python or browser consumer:
```js
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const packed = Buffer.concat([ct, cipher.getAuthTag()]); // now matches doFinal()
```
Unpacking a concatenated blob for Node:
```js
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(packed.subarray(packed.length - 16)); // must come before final()
const pt = Buffer.concat([
decipher.update(packed.subarray(0, packed.length - 16)),
decipher.final(),
]);
```
The ordering constraint is real: call `setAuthTag()` after `final()` and Node throws `Unsupported state or unable to authenticate data` even when every byte is correct.
### Tag length is configurable, and the unit differs by API
GCM permits tags of 128, 120, 112, 104 or 96 bits, with 64 and 32 reserved for constrained applications (SP 800-38D, Appendix C). Almost everyone uses 128, and the trouble is how each API asks for it. Java's `new GCMParameterSpec(128, iv)` takes its first argument in bits. Web Crypto's `{ name: 'AES-GCM', iv, tagLength: 128 }` is also in bits and defaults to 128. Node's `createCipheriv(algo, key, iv, { authTagLength: 16 })` is in bytes.
`new GCMParameterSpec(16, iv)` is a legal-looking Java line that asks for a 16-bit tag; some JDKs reject it, and where it is accepted you have swapped your integrity guarantee for a one-in-65,536 coin flip. When the two sides disagree on tag length the packed lengths differ too, so the receiver slices at the wrong boundary and gets an authentication failure that has nothing to do with the key.
## 7. You have a passphrase, not a key
If either side takes a human-typed string, there is a key derivation function between that string and AES, and a KDF mismatch is invisible. It never errors. It returns 32 perfectly good bytes that happen to be the wrong 32 bytes, and the failure surfaces one layer down as (you know this one) a padding error.
### PBKDF2 needs four things to line up
- The salt. In the OpenSSL `Salted__` format it is 8 bytes inside the ciphertext; in our tools' passphrase format it is a 16-byte prefix; in hand-rolled schemes it is frequently a hardcoded constant.
- The iteration count. `openssl enc -pbkdf2` defaults to 10,000. OWASP currently recommends 600,000 for PBKDF2-HMAC-SHA256, which is what our passphrase mode uses. Frameworks pick their own numbers.
- The hash. SHA-1 versus SHA-256 versus SHA-512. Older code and some mobile SDKs still default to SHA-1.
- The output length. Thirty-two bytes for AES-256, sixteen for AES-128. Some schemes derive key and IV together from one longer call, which never matches a plain 32-byte derivation.
### EVP_BytesToKey, and why CryptoJS keeps not working
`cryptojs aes decrypt not working` is usually one specific mismatch. `CryptoJS.AES.encrypt(text, "passphrase")` does not use PBKDF2. It uses EVP_BytesToKey, OpenSSL's pre-1.1 derivation, with MD5 and a single iteration.
EVP_BytesToKey also does something PBKDF2 does not: it derives the key *and the IV* from the passphrase and salt in one pass. That is why an OpenSSL `Salted__` file carries no separate IV field, and why reproducing CryptoJS output with PBKDF2 plus a random IV is wrong twice over.
The format is recognisable on sight: the 8 ASCII bytes `Salted__` followed by an 8-byte salt, base64-encoded, always begin `U2FsdGVkX1`. If your ciphertext starts that way it is passphrase-derived and you need to know which derivation; the [AES decrypt tool](/tools/aes-decrypt) detects the prefix and switches between the three without code changes.
### Why the same password gives different keys
There is no such thing as "the AES password". Every library invented its own path from string to key:
| Producer | Derivation | Result for one passphrase |
|---|---|---|
| CryptoJS `AES.encrypt(text, pass)` | EVP_BytesToKey, MD5, 1 iteration | key A |
| `openssl enc` 1.0.2 and earlier | EVP_BytesToKey, MD5, 1 iteration | key A |
| `openssl enc` 1.1+ without `-pbkdf2` | EVP_BytesToKey, SHA-256, 1 iteration | key B |
| `openssl enc -pbkdf2` | PBKDF2-HMAC-SHA256, 10,000 iterations | key C |
| Our passphrase mode | PBKDF2-HMAC-SHA256, 600,000 iterations | key D |
| Java, Python, Go | no default at all; you write the derivation | whatever you wrote |
Four keys from one password before anyone has made a mistake. The 1.0.2-to-1.1 upgrade changed the default digest from MD5 to SHA-256, which is why ciphertext from old scripts stopped decrypting with the same command on a newer box. If you inherited data and nobody remembers the toolchain, try the derivations in that order. There are only three to test, so it finishes in minutes.
## 8. What the transport did to your bytes
Ciphertext is uniformly random binary, which makes it hostile to anything that treats bytes as text. A large share of AES failures never involve the cipher.
### Base64 variants and missing padding
Standard base64 (RFC 4648 §4) uses `+` and `/`; the URL-safe variant (§5) uses `-` and `_`. A URL-safe string handed to a standard decoder either throws or, in lenient decoders, silently discards the offending characters and returns short, misaligned bytes, which is why Java ships `Base64.getUrlDecoder()` and `Base64.getDecoder()` as separate objects. Some encoders also drop the trailing `=`, some decoders insist on it, and JWT-adjacent code paths strip it by default.
Before suspecting the key, decode the ciphertext and check its length against the mode:
- CBC and ECB want a nonzero multiple of 16. Anything else is truncation or a decode problem rather than a key problem.
- GCM ciphertext is the length of the plaintext, plus 16 for the tag, plus 12 at the front if the nonce is prepended.
- CTR accepts any length, so this check tells you nothing.
The [Base64 decoder](/tools/base64-decode-encode) gives you the byte count in one paste, often the fastest measurement in the whole investigation.
### Newlines, smart quotes and the UTF-8 round trip
`openssl base64` wraps output at 64 columns unless you pass `-A`, and some decoders skip embedded newlines while others reject them, so the same file decodes on one machine and fails on another. Copying through a chat client or a document editor turns straight quotes curly and hyphens into en dashes, and the difference is nearly invisible in a terminal.
The unrecoverable one is a UTF-8 round trip. If raw AES output is ever held as a string without being encoded first (`new String(cipherBytes)` in Java, `bytes.decode('utf-8', errors='replace')` in Python, a `TextDecoder` anywhere), every byte sequence that is not valid UTF-8 collapses to U+FFFD, and encoding it back gives you `EF BF BD` where your data used to be. Since roughly half of random bytes are non-ASCII, most of the ciphertext is destroyed and no key recovers it; the [UTF-8 and UTF-16 encoding guide](/blog/utf-8-utf-16-unicode-encoding-guide) covers why the loss is one-way. Binary ciphertext travels as base64, as hex, or as binary. Putting it in a string is what destroys it.
### Database columns
Storage applies the same damage more quietly. Ciphertext written to a `VARCHAR(255)` that is one block too long gets cut, and MySQL outside strict mode does it without an error. The tail is where the padding block and the GCM tag live, so a row written "successfully" months ago now fails, and if the cut landed on a 16-byte boundary the length check above will not catch it either. Charset conversion does the rest: a `latin1` column receiving UTF-8 bytes rewrites your data on the way in.
Store ciphertext in `VARBINARY`, `BLOB` or `bytea`, or store base64 in a text column with room to spare.
## 9. A bisection workflow that finds it in five minutes
Every section above narrows one variable. Running them in order against a reference implementation you control converges fast, and the browser tools work well as that reference because they run entirely in your browser, where your key and ciphertext never leave the page, and you can change one setting at a time and see the bytes.
0. **Step 0: measure the shape.** Decode the ciphertext and note the byte count, the first few bytes, and whether it begins `U2FsdGVkX1`. Check the count against section 8. If it is not a multiple of 16 and you believe you are in CBC, stop: this is a transport bug.
1. **Step 1: encrypt a known plaintext.** In the [AES encrypt tool](/tools/aes-encrypt), encrypt a short known string with the parameters you *believe* production uses, then compare the shape of the two outputs rather than their values: total length, prefix bytes, presence of a salt header. A mismatch means your assumption about the format or the KDF is wrong, and no amount of key-fiddling fixes it.
2. **Step 2: cycle the derivations.** For passphrase-derived data, run PBKDF2 with the exact iteration count, then EVP-SHA256, then EVP-MD5 in the [AES decrypt tool](/tools/aes-decrypt). Exactly one can be right. If none works, the bug is above the KDF.
3. **Step 3: remove every convention.** Switch to a raw key, turn on bare ciphertext, supply the IV explicitly. You are now stating exactly which bytes are key, IV and ciphertext, with nothing inferred. If it decrypts here but not in your code, your bug is a framing bug (an unstripped IV prefix, a tag in the wrong place) rather than a cryptographic one.
4. **Step 4: flip the mode.** Try CBC, then CTR, then GCM against the same bytes. CTR returning readable text where CBC failed means you have a mode mismatch and nothing else.
5. **Step 5: read the garbage.** First block bad and the rest clean means the IV. First block clean and the rest bad means you decrypted CBC as ECB with a zero IV. Everything bad means the key or the derivation.
## 10. FAQ
### Why does my AES code work locally but fail in production?
AES code that works locally and fails in production means the environment changed something that is not in source control. Usual suspects, in order: the key arrived from an environment variable or secret manager with a trailing newline; Java's platform default charset differs between laptop and container, so `getBytes()` produced different bytes (section 3); production OpenSSL is 1.1+ while your local scripts targeted 1.0.2, changing the EVP_BytesToKey digest from MD5 to SHA-256; or a database column truncates the ciphertext in one environment only. Print the key length and ciphertext length in bytes on both sides first, because those two numbers usually settle it.
### I encrypted in Node and can't decrypt in Java. Where do I start?
When Node encrypts and Java cannot decrypt, start with the GCM tag, the most common and least obvious cause. Node returns ciphertext and tag separately; Java's `doFinal()` expects them concatenated as `ciphertext || tag`, and section 6 shows the bytes are otherwise identical. On CBC instead, start with the IV convention: did Node prepend it, and does the Java side strip 16 bytes before decrypting? Third is the key itself, where `Buffer.from(k, 'hex')` and `k.getBytes(StandardCharsets.UTF_8)` produce different lengths from the same string.
### Is Java's `PKCS5Padding` the same as PKCS#7?
For AES, Java's `PKCS5Padding` is PKCS#7 in effect. PKCS#5 (RFC 8018) is defined only for 8-byte blocks; PKCS#7 (RFC 5652) generalises the scheme to block sizes from 1 to 255 bytes. Java's `PKCS5Padding` applied to a 16-byte block cipher implements PKCS#7 behaviour, and the name is a historical leftover, so this is never your bug. `NoPadding` is: it requires plaintext already a multiple of 16, and on decryption it hands the padding back as data, so you see plausible text with trailing bytes like `\x05\x05\x05\x05\x05`.
### My key is 32 characters but AES says the key length is invalid. Why?
A length error means the library got a byte count that is not 16, 24 or 32. With a 32-character string that is usually a trailing newline (33 bytes), a `0x` prefix that makes the string invalid hex, or a non-ASCII character occupying two or three bytes in UTF-8. The more dangerous variant is getting no error at all: 32 hex characters decode to 16 valid bytes and 32 base64 characters decode to 24 valid bytes, both legal AES lengths. The library accepts them, uses the wrong key, and hands you a padding failure instead. Whatever the string looks like, the byte count is the number to check.
### Decryption "succeeded" but the output is garbage. What went wrong?
Decryption that "succeeded" and handed you garbage means you are in a mode that verifies nothing. CTR and ECB never throw, and CBC throws only when the final byte pattern fails the padding check, which a wrong key passes a little under 0.4% of the time. Read the shape: first 16 bytes corrupt and the rest clean means the IV; first 16 clean and the rest corrupt means you decrypted CBC ciphertext as ECB with a zero IV; uniformly corrupt means the key or the derivation. Readable text with a few odd trailing bytes means `NoPadding` on padded data. The durable fix is GCM, so that "succeeded" means something.
### Can I still decrypt if I lost the IV?
Without the IV you can still decrypt everything in CBC except the first 16 bytes. Blocks 2 onward are recovered as `D(C_i) XOR C_{i-1}`, and every input to that is already in the ciphertext, so only the first block needs the IV. If you also know how the plaintext starts, say a JSON blob beginning `{"userId":`, you can recover the IV outright as `D(C1) XOR P1`. In CTR the IV seeds the entire keystream, so losing it loses everything. In GCM the nonce feeds both the counter and the tag, so there is no partial recovery.
### Can I recover the plaintext if the GCM tag was truncated or dropped?
A truncated or dropped GCM tag still leaves the plaintext recoverable: mathematically yes, practically with effort. GCM is CTR mode underneath, so the key and nonce alone reproduce the keystream. No mainstream library will do it for you: Java, Go, Python and Web Crypto all refuse to release plaintext without a valid tag, by design. The workaround is to decrypt the same bytes as AES-CTR with the initial counter block set to the 12-byte nonce followed by `00000002`, which is where GCM's first data block starts. You get the data back and give up every integrity guarantee, so treat the result as untrusted. If you still have all 16 tag bytes and authentication fails anyway, the tag is not missing and something else on this page is your bug. Take it to the [AES decrypt tool](/tools/aes-decrypt) and start at step 0.
---
### Advanced Base64: MIME, Data URLs, Performance & Security
URL: https://go-tools.org/blog/base64-complete-guide
Implement Base64 in JavaScript and Python, optimize data URLs, choose standard vs URL-safe variants, and avoid common security pitfalls.
# Base64 in Production: MIME, Data URLs, Performance Traps & Security Pitfalls
> **New to Base64?** If you're just getting started, read our [beginner-friendly introduction to Base64 encoding](/blog/understanding-base64) first.
Base64 encoding is everywhere in modern web development, from email attachments to data URLs, from API authentication to image embedding. This guide focuses on practical implementation, performance optimization, and the advanced details you need for production use.
## What is Base64?
Base64 is a binary-to-text encoding scheme that converts binary data into a safe ASCII string using 64 printable characters. For a thorough introduction to Base64 fundamentals — including the character set, why it exists, and how the encoding algorithm works step by step — see our [beginner-friendly Base64 guide](/blog/understanding-base64).
## How Base64 Encoding Works
### The Algorithm Step by Step
1. **Take 3 bytes of input** (24 bits total)
2. **Split into 4 groups of 6 bits each**
3. **Map each 6-bit value to a Base64 character**
4. **Add padding if necessary**
### Example: Encoding "Man"
```
M = 01001101 (77 in decimal)
a = 01100001 (97 in decimal)
n = 01101110 (110 in decimal)
```
**Step 1**: Concatenate the bits
```
010011010110000101101110
```
**Step 2**: Split into 6-bit groups
```
010011 | 010110 | 000101 | 101110
```
**Step 3**: Convert to decimal and map to Base64
```
010011 = 19 → T
010110 = 22 → W
000101 = 5 → F
101110 = 46 → u
```
**Result**: "Man" becomes "TWFu"
### Handling Padding
When the input length isn't divisible by 3, padding is needed:
- **1 byte remaining**: Add 2 padding characters (`==`)
- **2 bytes remaining**: Add 1 padding character (`=`)
## Base64 in MIME (Email Attachments)
### The MIME Standard
MIME (Multipurpose Internet Mail Extensions) was one of the first major applications of Base64. Email was originally designed for 7-bit ASCII text, but users needed to send binary files like images and documents.
### How Email Attachments Work
When you attach a file to an email:
1. The file is read as binary data
2. Base64 encoding converts it to text
3. The encoded text is embedded in the email
4. The recipient's email client decodes it back to binary
### MIME Example
```
Content-Type: image/jpeg
Content-Transfer-Encoding: base64
/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEB...
```
## Base64 in Data URLs
### What are Data URLs?
Data URLs allow you to embed small files directly in HTML, CSS, or JavaScript using the `data:` scheme:
```
data:[mediatype][;base64],
```
### Common Use Cases
**Embedding Images in CSS**
```css
.icon {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...);
}
```
**Inline SVG Icons**
```html
```
**Small JavaScript Files**
```html
```
## Base64 Variants
### Standard Base64 (RFC 4648)
- Uses `+` and `/` as the last two characters
- Uses `=` for padding
- Safe for most applications
### URL-Safe Base64 (RFC 4648 Section 5)
- Replaces `+` with `-`
- Replaces `/` with `_`
- May omit padding (`=`)
- Safe for URLs and filenames
### Comparison Example
```
Standard: "??>" → Pz8+
URL-Safe: "??>" → Pz8-
```
## Practical Code Examples
### JavaScript Implementation
```javascript
// Encoding
function encodeBase64(str) {
return btoa(unescape(encodeURIComponent(str)));
}
// Decoding
function decodeBase64(str) {
return decodeURIComponent(escape(atob(str)));
}
// Usage
const original = "Hello, World!";
const encoded = encodeBase64(original);
const decoded = decodeBase64(encoded);
console.log(`Original: ${original}`);
console.log(`Encoded: ${encoded}`);
console.log(`Decoded: ${decoded}`);
```
### Python Implementation
```python
import base64
# Encoding
def encode_base64(data):
if isinstance(data, str):
data = data.encode('utf-8')
return base64.b64encode(data).decode('ascii')
# Decoding
def decode_base64(encoded_data):
return base64.b64decode(encoded_data).decode('utf-8')
# Usage
original = "Hello, World!"
encoded = encode_base64(original)
decoded = decode_base64(encoded)
print(f"Original: {original}")
print(f"Encoded: {encoded}")
print(f"Decoded: {decoded}")
```
## Real-World Applications
### Web API Authentication
Many APIs use Base64 for basic authentication:
```javascript
const username = "user";
const password = "pass";
const credentials = btoa(`${username}:${password}`);
fetch('/api/data', {
headers: {
'Authorization': `Basic ${credentials}`
}
});
```
### JSON Web Tokens (JWT)
JWTs use Base64URL encoding for their header and payload:
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0...
```
### Image Embedding
Embedding small images directly in HTML:
```html
```
## Performance Considerations
### Size Increase
Base64 encoding increases data size by approximately **33%**:
- 3 bytes of binary data → 4 bytes of Base64 text
- Overhead ratio: 4/3 = 1.33
### When to Use Base64
**Good for:**
- Small files (< 10KB)
- Reducing HTTP requests
- Embedding in CSS/HTML
- Text-based protocols
**Avoid for:**
- Large files
- Frequently changing content
- When binary transfer is available
- Performance-critical applications
### Caching Implications
- Base64 data URLs can't be cached separately
- Changes to embedded data require cache invalidation
- Consider external files for frequently updated content
## Best Practices
### 1. Choose the Right Variant
- Use standard Base64 for general purposes
- Use URL-safe Base64 for URLs and filenames
- Consider omitting padding when safe
### 2. Optimize for Performance
- Keep embedded data small (< 10KB)
- Use external files for large or frequently changing content
- Consider gzip compression for Base64 text
### 3. Security Considerations
- Base64 is encoding, **not encryption**
- Don't use Base64 to hide sensitive data
- Validate decoded data before use
### 4. Debugging Tips
- Use online tools for quick encoding/decoding
- Check for proper padding
- Verify character set compatibility
- When debugging config files that contain Base64 values, a [JSON5/JSONC-aware formatter](/blog/json5-jsonc-formatting-guide) can help you inspect them without stripping comments
## Try It Yourself
*Encode and decode Base64 instantly with our [Base64 Encoder/Decoder](/tools/base64-decode-encode) — supports UTF-8, URL-safe variants, and real-time conversion. 100% in your browser.*
## Frequently Asked Questions
### Does Base64 encoding provide any security?
No — Base64 is an encoding scheme, not encryption. Anyone can decode Base64 data without a key. It is designed for safe data transport, not confidentiality. Never use Base64 to "protect" sensitive information like passwords or API keys. For security, use proper encryption algorithms like AES-256 or TLS for data in transit.
### Why does Base64 increase data size by about 33%?
Base64 represents every 3 bytes of binary data as 4 ASCII characters. This 3-to-4 ratio means the output is always approximately 4/3 (133%) of the input size — a 33% increase. This overhead is the trade-off for being able to safely transmit binary data through text-only channels like email or JSON.
### What is the difference between standard Base64 and URL-safe Base64?
Standard Base64 uses `+` and `/` characters, which have special meanings in URLs. URL-safe Base64 (RFC 4648) replaces them with `-` and `_`, making the output safe for use in URLs, query parameters, and filenames without additional [percent-encoding](/tools/url-decoder-encoder). Most modern APIs prefer URL-safe Base64 for tokens and identifiers.
### When should I use Base64 Data URLs instead of regular image files?
Use Data URLs for small images under 2-4KB, like icons and simple logos, to eliminate an HTTP request. For larger images, regular files with proper caching are more efficient — Data URLs cannot be cached independently, increase HTML size by 33%, and must be re-downloaded with every page load.
### Can I use Base64 to encode non-ASCII text like Chinese or emoji?
Yes, but you must first convert the text to bytes using a character encoding like UTF-8, then Base64-encode those bytes. When decoding, reverse the process: Base64-decode to bytes, then interpret the bytes as UTF-8 text. Most modern libraries handle this automatically, but always specify UTF-8 explicitly to avoid encoding errors.
## Conclusion
Base64 encoding is a fundamental technology that bridges the gap between binary data and text-based systems. From its origins in email attachments to modern web applications, Base64 continues to be an essential tool for developers.
**Key takeaways:**
- Base64 converts binary data to safe ASCII text
- It's essential for email attachments and data URLs
- Choose the right variant for your use case
- Consider performance implications for large data
- Remember: it's encoding, not encryption
---
### Image to Base64 & Data URIs: When to Inline Images (2026)
URL: https://go-tools.org/blog/base64-images-data-uri-inline-guide
Should you convert an image to Base64? See when data URIs help, the 33% size cost, CSS/HTML inlining, caching tradeoffs, and when a normal image file wins.
When you convert an **image to Base64**, you get a **data URI**: a string like `data:image/png;base64,iVBORw0KGgo…` that you can paste straight into an HTML `src` or a CSS `url()`. The browser decodes it on the spot and shows the picture with no separate download. No file to host, no extra request.
So should you do it? Here is the short rule. Inline an image as Base64 when it is small (under about 2 KB), rarely changes, and you want to skip one HTTP request. Think tiny icons and logos. For everything else, keep it as a normal image file: large images, anything reused across pages, anything you want the browser to cache. The catch is that Base64 makes a file about 33% larger, and once that text is embedded in your HTML or CSS it can no longer be cached on its own.
If you want the exact numbers for a specific file, the [Image to Base64 converter](/tools/image-to-base64) does the encoding in your browser and shows the precise size increase, so you can decide with real data instead of a rule of thumb. This guide covers what that data URI actually is, the math behind the size tax, a decision matrix for when inlining pays off, and the cases where a plain file wins.
## What "image to Base64" actually produces: the data URI
Converting an image to Base64 does not give you a file. It gives you one long string that follows the data URI format defined in RFC 2397 (see [MDN's `data:` URL reference](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data) for the full spec). The string has three parts:
```
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA…
└──┬─┘ └───┬───┘ └─┬──┘ └─────────┬──────────┘
data: MIME type marker the encoded image bytes
```
The MIME type tells the browser what kind of image it is decoding. The common ones for images are `image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/svg+xml`, and `image/x-icon` for favicons. The `;base64,` marker says the payload that follows is Base64 rather than plain text. Everything after the comma is the image, re-expressed as printable ASCII.
That last part matters for privacy. The conversion runs entirely in your browser through the `FileReader` API's `readAsDataURL`, so nothing is uploaded to a server. You can drop a pre-launch screenshot or unreleased artwork into the tool and watch the Network tab stay empty. For the mechanics of how raw bytes become that ASCII string, [understanding Base64](/blog/understanding-base64) covers the encoding from the ground up, and the [complete Base64 guide](/blog/base64-complete-guide) extends the same data-URL idea to fonts, PDFs, and other file types.
### A real example: a 68-byte transparent PNG
Here is the smallest practical case, a 1×1 transparent PNG, 68 bytes on disk, written out as a complete data URI:
```
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==
```
Paste that into a browser address bar and you will see (well, not see, since it is transparent) a valid image render with zero network activity. Notice the trailing `==`: that is padding, which we will get to. This is also exactly what text Base64 looks like, just applied to image bytes instead of text. If you only need to encode or decode plain text strings, the [Base64 encode/decode](/tools/base64-decode-encode) tool handles that case.
## The 33% size tax (and why it compounds)
Base64 works in fixed groups: every 3 bytes of binary become 4 ASCII characters. Four-thirds is roughly 1.33, which is where the +33% figure comes from. Add a byte or two of padding plus the `data:image/png;base64,` prefix and the overhead is slightly higher for tiny files. A concrete example: a 9 KB PNG becomes about 12 KB of text.
Why exactly 3-to-4? Base64 uses a 64-character alphabet: `A`–`Z`, `a`–`z`, `0`–`9`, plus `+` and `/`. Sixty-four symbols is 6 bits of information per character, while binary bytes are 8 bits each. The lowest common multiple of 6 and 8 is 24 bits, which is 3 bytes or 4 Base64 characters, so the encoder works through the image 24 bits at a time. When the image length is not a clean multiple of 3, one or two `=` characters pad the final group. That math is fixed; no encoder setting shrinks the 33%.
That 33% is the visible cost. The hidden cost is that it compounds, and this is the part most "just inline it" advice skips:
- **The image is re-downloaded whenever the containing file changes.** An external `logo.png` is its own resource. Inline it into `styles.css`, and now any edit to that stylesheet, even a one-line color tweak, invalidates the cache for the image too. Visitors re-download the picture they already had.
- **It cannot be cached independently.** A normal image file is fetched once and reused across every page and every visit. An inlined data URI is part of the document, so it ships again on every page that embeds it and on every cache miss of that document.
- **CSS is render-blocking.** The browser will not paint until it has the CSS. Stuff a large data URI into a stylesheet and you have made a render-blocking resource bigger, delaying first paint for the whole page.
### Does gzip or brotli cancel the 33% out?
Partly, not fully. Base64 text is repetitive enough that gzip and brotli compress it well, clawing back a good chunk of the inflation over the wire. But two things remain true. First, the compressed Base64 is usually still a little larger than the compressed original binary, because you have handed the compressor a less efficient starting point. Second, and this is the part that bites, compression does nothing about caching or render-blocking. A smaller-over-the-wire data URI is still re-downloaded with its host file and still cannot be cached on its own.
So compressing the bytes is not the same as removing the cost of inlining them. If the distinction between minifying, gzipping, and brotli is fuzzy, the [code minification guide](/blog/code-minification-guide-css-js-html) lays out how those layers stack, and why squeezing the bytes never fixes the caching problem that inlining creates.
## When to use a Base64 image (the decision matrix)
The whole decision comes down to a handful of factors. Here they are side by side:
| Factor | Lean toward inlining (Base64) | Lean toward a normal file |
|--------|-------------------------------|---------------------------|
| **Size** | Under ~2 KB (green) | Over ~10 KB (red); 2–10 KB is a judgment call (amber) |
| **Reuse** | One page, a place or two | Repeated across many pages |
| **Change frequency** | Almost never changes | Edited often |
| **Context** | HTML email, self-contained widget or bookmarklet, JSON/API payload, a critical above-the-fold icon worth one saved request | Content images, shared cacheable assets |
Those size thresholds are not arbitrary. They mirror the traffic-light badge built into the [Image to Base64 converter](/tools/image-to-base64): green under 2 KB, amber up to 10 KB, red above. The tool reads your actual file and tells you which bucket it lands in.
### A simple rule of thumb
If you remember one line, make it this: **under ~2 KB and used in only one or two places, inlining usually pays off; over ~10 KB or reused across pages, a normal cached file almost always wins.** The 2–10 KB middle is where you weigh the saved request against the lost cache for your specific situation.
### Good fits in detail
A few cases where Base64 genuinely earns its keep:
- **HTML email.** Many email clients block externally hosted images by default for privacy, which breaks any layout that depends on a remote logo. A small inlined data URI renders immediately with no server fetch. Keep these to logos and icons; never inline a photograph into an email.
- **Self-contained widgets and bookmarklets.** A bookmarklet or an embeddable widget has to work with zero external dependencies. Inlining its icons keeps everything in a single droppable file.
- **JSON and API payloads.** Shipping a thumbnail inside a JSON document or a config file is sometimes the cleanest option: one round trip, one object, no second request to wire up.
- **A critical above-the-fold icon.** When a tiny logo is part of your Largest Contentful Paint and you want to shave one request off the critical path, inlining can help. Emphasis on *tiny*.
One pattern ties these together: in each case the asset travels *with* something else and would otherwise need its own delivery channel. An email cannot rely on your CDN, a bookmarklet has no second file to fetch, and a JSON response arrives as a single payload. So the alternative to inlining here is not a cached file but a missing image, which changes the calculus entirely. The useful question for a Base64 fit is not only whether the asset is small, but whether a separate file is even an option in the first place.
## When NOT to inline: caching, lazy loading, and Core Web Vitals
The flip side is longer, because inlining quietly disables several things the browser does well.
**You lose independent caching.** This stings most for returning visitors. A normal image sits in their cache after the first visit and loads instantly forever after. An inlined image has no independent cache entry; it rides along with the document every single time, so a repeat visitor pays the byte cost again and again.
**You lose lazy loading.** The `loading="lazy"` attribute lets the browser defer images that are below the fold until the user scrolls near them. A data URI is parsed and "downloaded" the instant the HTML is read, so there is nothing to defer. Inline a dozen below-the-fold images and you have forced all of them into the initial load.
**You enlarge render-blocking resources.** As noted earlier, a data URI inside CSS bloats a resource that blocks first paint. The bigger that stylesheet, the longer the page sits blank.
**Decoding is more expensive on mobile.** A data URI is Base64-decoded every time its document loads, and on low-end phones that extra CPU work adds up. Worse, the bytes never land in the browser's disk cache, so a heavy inlined image is re-decoded on each visit instead of being cached and decoded once like a normal file.
There is also a historical reason this advice has shifted. The original case for inlining, made loudly in the HTTP/1.1 era, was request reduction: each connection could fetch one resource at a time, so a page with 40 small icons paid 40 round trips. HTTP/2 changed that by multiplexing many requests over a single connection, which made extra small files cheap. The big payoff of inlining, fewer requests, mostly evaporated, while the costs stayed: lost caching, no lazy loading, bigger render-blocking files. If you read older articles enthusiastic about Base64 sprites, weigh them against the protocol your site actually runs on today.
### The Core Web Vitals angle
Inlining cuts both ways on LCP ([Largest Contentful Paint](https://web.dev/articles/lcp)). For a small, above-the-fold image that *is* the LCP element, removing a request can nudge LCP earlier. But inline a large image and you do the opposite: you delay the document or stylesheet it lives in, pushing LCP later for the whole page. The size threshold decides which way it goes.
For CLS (Cumulative Layout Shift), inlining changes nothing about the core rule: an image still needs explicit `width` and `height` (or an aspect-ratio box) so the browser can reserve space before it renders. A data URI without dimensions shifts layout exactly like a remote image without dimensions.
A better lever than inlining is usually shrinking the source. Compressing an image before you encode it makes both the file and any resulting data URI smaller. The [browser vs Node image compression guide](/blog/image-compression-browser-vs-node) covers how to do that client-side or in a build step, and [WebP vs AVIF vs JPEG](/blog/webp-vs-avif-vs-jpeg-image-format-guide) helps you pick a format that is small to begin with.
## How to inline images in HTML, CSS, Markdown, and JSON
Once you have a data URI, here is how it drops into each context. These are the four ready-to-paste snippets the [Image to Base64 converter](/tools/image-to-base64) generates for you.
**HTML**: paste the URI into any `src`:
```html
```
**CSS**: wrap it in `url()` for a `background-image` (this is the canonical base64 image in CSS pattern):
```css
.icon {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0i…");
}
```
**Markdown**: a self-contained image link for READMEs, GitHub issues, and notebooks where you cannot host a file:
```markdown

```
**JSON**: an embedded asset inside an API or config payload:
```json
{ "icon": "data:image/png;base64,iVBORw0KGgo…" }
```
All four work anywhere a URL is accepted: `img src`, CSS `background`, `mask-image`, even a favicon ` `. Every modern browser supports the `data:` scheme.
### Generating these quickly
Building these by hand is error-prone: one wrong MIME type or a stray line break and the image silently fails to render. Drop your file into the [Image to Base64 converter](/tools/image-to-base64) and it produces all four snippets with their own copy buttons, plus the exact size increase so you know up front whether the asset belongs inline at all.
## SVG: the special case where Base64 usually loses
SVG breaks the usual logic, because SVG is text, not binary. Base64 exists to make binary data text-safe, but SVG is already XML text. Encoding it as Base64 just inflates a string that did not need encoding, and makes it unreadable in the process. So for SVG specifically, Base64 is almost always the wrong choice.
Compare three ways to inline the same icon:
```css
/* 1. Base64 data URI — adds the 33% tax to text that didn't need it */
.a { background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0i…"); }
/* 2. URL-encoded data URI — percent-encode a handful of characters, no 33% tax */
.b { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'…%3C/svg%3E"); }
/* 3. Inline directly in the HTML — fully styleable with CSS */
```
```html
```
Option 2 (URL-encoding) is usually smaller than option 1, stays human-readable, and compresses better. You only percent-encode the characters that would break the URI (`<`, `>`, `#`, and quotes) and leave the rest legible. The [URL encoder/decoder](/tools/url-decoder-encoder) approach is documented in the tool itself; reach for Base64 SVG only when a build pipeline specifically demands it.
### Why an inline `` often beats a Base64 PNG icon
If you are choosing between a Base64-encoded PNG icon and an inline ``, the SVG usually wins. It scales to any size without blurring and carries no 33% tax, and unlike any data URI you can style it with CSS, animate it, and recolor it with `currentColor`. A Base64 PNG is a fixed-resolution blob you cannot touch once encoded. Reserve raster Base64 for cases where you genuinely need a photograph or a raster screenshot inline.
## Decoding the other way: Base64 back to an image
The reverse problem is just as common: you have a Base64 string, pulled from an API response, a log line, or a stylesheet you are debugging, and you need to see the actual picture.
Two details trip people up. First, raw Base64 versus a full data URI. A complete data URI (`data:image/png;base64,…`) carries its own MIME type; a bare payload (`iVBORw0KGgo…`) does not. To render a bare payload you either prepend a correct `data:` prefix or let a tool infer the format from the leading bytes: `iVBORw0KGgo` means PNG, `/9j/` means JPEG, `R0lGOD` means GIF.
Second, line wrapping. Base64 from email or older tooling is often wrapped at 76 characters per RFC 2045. Those newlines must be stripped before decoding, or the string is invalid in an HTML attribute or `url()`.
In the browser you can hand a complete data URI straight to an ` `:
```html
```
On the server, Node reconstructs the file from the payload:
```js
import { writeFileSync } from "node:fs";
const b64 = "iVBORw0KGgoAAAANSUhEUgAA…"; // raw payload, no data: prefix
writeFileSync("output.png", Buffer.from(b64, "base64"));
```
For a no-code path, use the [Base64 to Image converter](/tools/base64-to-image): paste a string (with or without the prefix, line breaks and all), preview it, read its dimensions and MIME type, and download a real PNG, JPG, GIF, or SVG. It strips whitespace, tolerates a missing prefix, and detects the format from magic bytes automatically.
One sanity check worth doing on a decoded image: look at its reported dimensions. If you pulled one string out of a file that held several and the result is 1×1, you probably grabbed a tracking pixel instead of the asset you wanted. And remember that decoding is purely mechanical and lossless: a Base64 PNG comes back as the exact same PNG, byte for byte, with no recompression. The only thing that changed along the way was the container, a text string on the way out and a binary file on the way back.
## FAQ
### Should I convert my images to Base64?
Only when it is worth it: small (under ~2 KB), rarely-changing icons or logos where skipping one HTTP request matters, plus HTML email, self-contained widgets, and JSON payloads. Large images or anything reused across pages should almost always stay as normal files, so you keep caching and lazy loading.
### How much larger does Base64 make an image?
About +33%. Base64 encodes every 3 bytes of binary as 4 ASCII characters, plus a little padding and the `data:` prefix. A 9 KB PNG becomes roughly 12 KB of text. To [convert an image to Base64](/tools/image-to-base64) and see the exact increase for your file, the tool reports the precise number in its metadata bar.
### Does Base64 make images load faster?
For a very small above-the-fold icon it can, by saving one request's round trip. For larger or reused images it is usually slower: you lose independent caching, you cannot lazy-load it, and inlining it into CSS enlarges a render-blocking resource. Size is the deciding factor.
### Can I use a Base64 image in CSS?
Yes: `background-image: url("data:image/png;base64,…")`. It is fine for tiny icons. Just remember the data URI becomes part of the stylesheet, so the whole file re-downloads whenever the CSS changes, and the image cannot be cached separately from it.
### Should I use SVG or Base64 for icons?
Prefer an inline `` or a URL-encoded SVG data URI. SVG is text, scales cleanly, and carries no 33% tax, so it is usually smaller than a Base64 PNG and you can style it with CSS. Reach for Base64 only when you specifically need a raster icon.
### How do I convert a Base64 string back to an image?
In the browser, drop a full `data:image/…;base64,…` URI into an ` `. On a server, use `Buffer.from(b64, "base64")` to write the file. A raw payload needs a `data:` prefix added, and line-wrapped strings need their newlines stripped first. The [Base64 to Image tool](/tools/base64-to-image) handles all of that and lets you download the result.
---
### bcrypt 72-Byte Password Error: Why Short Passwords Fail Too
URL: https://go-tools.org/blog/bcrypt-72-byte-password-error-troubleshooting-guide
Even a 14-byte password throws "password cannot be longer than 72 bytes". Blame passlib's 255-byte probe, not your password. Hash bcrypt free online.
# bcrypt 72-Byte Password Error: Why Short Passwords Fail Too
Two different problems produce the same message, and only one of them has anything to do with your password.
If your password really is over the bcrypt 72 bytes limit, bcrypt reads the first 72 bytes and discards the rest. We hashed two 82-byte passwords that shared their first 72 bytes under a fixed salt. Both produced `$2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S`, and `bcrypt.compareSync(p2, hash(p1))` returned `true`. The second password logs into the first password's account.
If your password is obviously short and you still get this:
```
password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
```
then the message is wrong about the cause. Under passlib 1.7.4 with bcrypt 5.0.0, a 14-byte password raises it.
The culprit there is a fixed 255-byte self-test probe inside passlib. It runs once, when the backend initialises, before your password reaches the hashing call at all. bcrypt 5.0.0 rejects the probe, the exception escapes, and you read a complaint about a password nobody typed.
The `__about__` monkey patch that dominates search results for this error does not fix it. We re-ran it in a clean process with the patch applied before `import passlib`, and the ValueError came back unchanged.
## 30-second triage: which one are you
| Your password | When the error fires | Root cause | Go to |
|---|---|---|---|
| Longer than 72 bytes | When you call hash | Genuinely too long. bcrypt 5.0 raises, bcrypt 4.x truncates silently | Sections 2 and 3 |
| Under 72 bytes, using passlib | On the first call in the process | passlib's 255-byte probe. Nothing to do with your password | Section 4 |
| Contains Chinese, Japanese, or emoji | Looks short, is not | Characters are not bytes | Section 3 |
| Started failing after a dependency upgrade | After deploy | The bcrypt 5.0 breaking change | Sections 4 and 5 |
If you are in row 2, skip ahead. Nothing in the next two sections will help you, and the fix is different.
## What bcrypt's 72-byte limit does to your password
### Why bcrypt stops at 72
bcrypt is built on Blowfish, and it feeds your password in as the Blowfish key. Blowfish expands its key into a P-array of 18 subkeys, each 32 bits wide. That is 18 × 4 = 72 bytes of key material, and the expansion loop wraps back to the start of the key once it has filled all 18 slots.
The ceiling is structural rather than a lazy implementation or a buffer someone forgot to raise. Every conforming bcrypt implementation on every platform has the same limit, which is why you see the number 72 in Python, Node, Go, Java, and PHP alike.
### Two different passwords, one hash
bcrypt password truncation is a security property rather than a length inconvenience, which is what makes this worth checking on your own stack.
Using bcryptjs 3.0.3 with the fixed salt `$2a$10$abcdefghijklmnopqrstuv`, we hashed two passwords of 82 bytes each:
| Password | Value | Bytes |
|---|---|---:|
| p1 | `"A"×72 + "XXXXXXXXXX"` | 82 |
| p2 | `"A"×72 + "ZZZZZZZZZZ"` | 82 |
Both produced the same digest:
```
$2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S
```
Two different passwords, one hash. The consequence:
```js
bcrypt.compareSync(p2, hash(p1)) // true
```
An attacker who knows the first 72 bytes of a long passphrase can append anything at all and authenticate. Every byte past the boundary contributes exactly zero to the strength of the stored hash, no matter how carefully your users chose them. If you want to check a hash you already have against a candidate password without wiring up a script, you can [generate and verify bcrypt hashes in the browser](/tools/bcrypt-generator) and watch the same behaviour yourself.
### Where the boundary falls
We narrowed the cutoff one byte at a time, keeping a shared prefix and changing exactly one byte after it:
| Identical prefix bytes | Byte N+1 differs at | Same hash? |
|---:|---:|---|
| 70 | 71 | `false` |
| 71 | 72 | `false` |
| **72** | **73** | **`true`** |
| 73 | 74 | `true` |
Byte 72 still counts. Byte 73 is the first one that does not. The cutoff is abrupt, with no partial mixing on either side of it, and the same comparison is cheap to run against your own library.
## Characters are not bytes
bcrypt counts UTF-8 bytes, and your users type characters. For ASCII the two numbers happen to coincide, which is exactly why this bites teams the moment they ship outside an English-speaking market.
| Character type | Example | Bytes per character | 72 bytes equals |
|---|---|---:|---:|
| ASCII Latin letters | `A` | 1 | 72 characters |
| Chinese Han characters | `密` | 3 | 24 characters |
| Japanese kana | `あ` | 3 | 24 characters |
| Emoji | `🔒` | 4 | 18 characters |
| Cyrillic | `я` | 2 | 36 characters |
| German umlauts | `ü` | 2 | 36 characters |
We confirmed both extremes: with a Chinese password, differences after the 24th character are ignored (`true`), and with an emoji password, differences after the 18th are ignored (`true`).
A 25-character Chinese passphrase looks generous in a password field. It has already crossed the line. A user who picks 20 emoji has been over the limit for two characters and will never be told.
### Measuring byte length in your own code
Length checks written against character counts will pass while the underlying value is already too long. Measure bytes:
```python
# Python
len(pw.encode("utf-8"))
```
```js
// Node.js
Buffer.byteLength(pw, "utf8")
```
```go
// Go
len([]byte(pw))
```
In browsers without `Buffer`, `new TextEncoder().encode(pw).length` gives the same number. Put this check in front of your hashing call and return a real validation message, rather than letting the library decide for you at 3 a.m. If you are also revisiting your minimum-length policy while you are in there, [how password strength is actually measured](/blog/password-entropy-explained) covers what a length rule buys you and what it does not.
## Why short passwords fail too: passlib's 255-byte probe
Your password is fourteen characters long and the library insists it is over 72 bytes. This is the case that sends most people to a search engine.
### Reproducing it
Three lines, on Python 3.14.5 with bcrypt 5.0.0 and passlib 1.7.4:
```python
from passlib.hash import bcrypt
bcrypt.hash("short-password") # 14 bytes
# ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
```
Fourteen bytes in, a complaint about 72 bytes out. The passlib bcrypt error is real, but the number in it describes something else entirely.
### The full call stack
Traced through passlib 1.7.4, this is what runs:
1. The first call triggers backend initialisation: `_calc_checksum` → `_stub_requires_backend()` → `set_backend()`.
2. `_load_backend_mixin` reads `bcrypt.__about__.__version__`. The attribute does not exist, so an `AttributeError` is raised. passlib swallows it and prints `(trapped) error reading bcrypt version`.
3. Initialisation continues into `_finalize_backend_mixin` (`passlib/handlers/bcrypt.py:421`), which calls `detect_wrap_bug(IDENT_2A)`.
4. `detect_wrap_bug` (same file, `:378`) verifies a fixed 255-byte probe.
5. bcrypt 5.0.0 raises `ValueError` for anything over 72 bytes, so the probe blows up on itself.
6. The exception propagates out to your call site. You see a message about 72 bytes that was never about your input.
The whole sequence happens once per process, on the first hash or verify. That is why the failure is so reliably reproducible and so completely insensitive to what you pass in.
### What the probe looks like
```python
secret = (b"0123456789" * 26)[:255]
```
That constant comes from the wraparound bug in BSD's bcrypt that [Openwall disclosed in 2012](https://www.openwall.com/lists/announce/2012/01/02/1), where long keys wrapped around and collapsed into weaker hashes. passlib checks at startup whether the backend it just loaded carries that flaw, and refuses to trust a backend that does.
`detect_wrap_bug` is not a passlib bug. It is defensive code doing exactly what it was written to do, using a test vector that has been valid for over a decade. What changed is that bcrypt 5.0.0 now treats a 255-byte input as an error rather than something to hash, which turns a passing self-test into an uncatchable one. The [pyca/bcrypt discussion in issue #1082](https://github.com/pyca/bcrypt/issues/1082) covers the collision between the two libraries.
### Why the `__about__` patch does not fix it
Search this error and you will be told, over and over, that bcrypt removed `__about__` and that restoring it repairs passlib. Both halves of that are wrong, and the measurement shows it:
| Version | `hasattr(bcrypt, "__about__")` | Prints trapped warning | passlib works |
|---|---|---|---|
| bcrypt 5.0.0 | `False` | Yes | No (ValueError) |
| bcrypt 4.3.0 | `False` | Yes | Yes |
bcrypt 4.3.0 has no `__about__` either. It prints the same `(trapped) error reading bcrypt version` line. And passlib runs on it without complaint. The missing attribute is therefore not the dividing line between working and broken. The `ValueError` behaviour change in 5.0.0 is.
Which means the popular patch cannot work, and it does not:
```python
import bcrypt, types
bcrypt.__about__ = types.SimpleNamespace(__version__=bcrypt.__version__) # before importing passlib
from passlib.hash import bcrypt as pl
pl.hash("short-password")
# still ValueError: password cannot be longer than 72 bytes, ...
```
We ran this in a clean process, with the patch applied before `import passlib`, precisely so nobody can attribute the failure to import ordering. It still fails. All the patch achieves is silencing a harmless warning. The 255-byte probe in step 4 is a separate stage that never consulted `__about__` in the first place, and it detonates either way.
## What bcrypt 5.0 actually changed
The bcrypt 5.0 breaking change is one line of behaviour with a large blast radius:
| Input | bcrypt 4.3.0 | bcrypt 5.0.0 |
|---:|---|---|
| 72 bytes | OK | OK |
| 73 bytes | OK (silently truncated) | `ValueError` |
| 100 bytes | OK (silently truncated) | `ValueError` |
| 255 bytes | OK (silently truncated) | `ValueError` |
The truncation in the 4.x column is not a figure of speech. Under 4.3.0, `hash(73 bytes)` and `hash(100 bytes)` built from the same prefix come out equal: `true`.
So bcrypt 5.0 is the more correct library here. Quietly discarding key material is a worse outcome than refusing to proceed, and refusing to proceed is what a hashing library should do when it cannot honour the input it was given. That does not make the upgrade painless. Code that was silently losing bytes for years now throws, and if that code path sits behind passlib, it throws before your input is even involved.
The damage from that table lands differently depending on how you call bcrypt. If you call it directly, the upgrade is visible: you get an exception on registration or login, at a code location you own, with a stack trace pointing at your own hashing call. Add a byte-length check in front of it and you are done in an afternoon.
If you go through passlib, the upgrade is invisible until it is total. The failure is not proportional to how many of your users have long passwords, because it does not depend on user input at all. Every hash and every verify in the process fails, from the first call onward, on a codebase where nothing about password handling changed. That is why this shows up as a deployment incident rather than a bug report, and why the error text sends people looking in exactly the wrong place.
## Fixing it
### If you can change the code
Drop passlib and call bcrypt directly. passlib's last release was 1.7.4 and the project has been quiet for a long time, so the layer buys you very little on a project that only needs bcrypt:
```python
import bcrypt
password = "correct horse battery staple".encode("utf-8")
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
bcrypt.checkpw(password, hashed) # True
```
`hashpw` and `checkpw` both take bytes, so encode at the boundary and keep the rest of your code working in `str`. There is no backend detection and no self-test probe, so nothing can fail on a password you never supplied. If you want to eyeball the resulting hash, or verify one your app produced, the [bcrypt generator](/tools/bcrypt-generator) runs entirely in your browser. Servers using bcrypt for HTTP Basic Auth have the same structural constraint in a different file format, which [the htpasswd guide](/blog/htpasswd-http-basic-auth-guide) walks through.
### If you cannot change the code today
Pin below 5:
```
bcrypt<5
```
We verified bcrypt 4.3.0 with passlib 1.7.4 and it works. It is a tourniquet rather than a repair, though. You are staying on a version whose behaviour with a bcrypt long password is to silently discard bytes, which is the exact problem 5.0 was released to stop. Put a date on the pin and plan the move.
### If your users really do type long passphrases
Hash the password once with SHA-256 first, base64-encode the digest, then feed that to bcrypt:
```python
import base64, hashlib, bcrypt
def prehash(password: str) -> bytes:
return base64.b64encode(hashlib.sha256(password.encode("utf-8")).digest())
hashed = bcrypt.hashpw(prehash(pw), bcrypt.gensalt(rounds=12))
bcrypt.checkpw(prehash(pw), hashed)
```
The output is always 44 bytes, comfortably under 72, whatever the input length. And it restores the property truncation destroyed: the two 82-byte passwords from the opening section, run through this, give `checkpw(prehash(p2), hash(prehash(p1)))` = `False`. The collision is gone.
The base64 step is doing real work, so do not drop it. A raw SHA-256 digest is arbitrary binary and can contain NUL bytes, which bcrypt implementations handle inconsistently. base64 gives you a NUL-free ASCII string of fixed length. Apply the same function on registration and on login, or every existing hash stops verifying.
### What not to do
The `__about__` monkey patch does not work. Section 4 has the measurement. If someone on your team is about to paste it in, that table will save them an afternoon.
Truncating with `pw[:72]` yourself is worse than doing nothing. It converts a loud failure back into a silent one, and it recreates the collision from section 2 in your own code. You would be hand-rolling the behaviour bcrypt 5.0 now refuses to perform, and unlike the library, your version will never warn anyone. If you need long passwords to work, pre-hash. If you do not, validate the byte length and reject with a clear message.
## What about hashes already in your database
### Which rows are affected
Only accounts whose owners registered with a password over 72 bytes. For most consumer products that is a small set, and for anything ASCII-only it usually means passphrase enthusiasts. For products with users typing Chinese, Japanese, or emoji, section 3 applies and the affected set can be much larger than a byte-blind audit suggests.
You cannot identify these rows from the hashes. A bcrypt digest is fixed-width and carries no record of how long its input was. If you logged password length at registration, that log is your only inventory. Most teams have not, and reconstructing it after the fact is not possible, so plan around not knowing rather than around a list.
### You cannot recompute in bulk
There is no plaintext to re-hash, which is the entire point of storing hashes. So the migration has to be lazy: upgrade each account the next time its owner successfully authenticates, while you briefly hold the plaintext in memory.
```python
def login(user, password: str) -> bool:
if not verify_legacy(password, user.password_hash):
return False
if needs_rehash(user.password_hash):
user.password_hash = hash_new_scheme(password)
save(user)
return True
```
Verify with the old scheme first, and only then re-hash. Reversing those two steps rewrites the stored hash before you have confirmed the password was correct. Store a scheme identifier alongside each hash so `needs_rehash` is a field comparison rather than a guess, and expect a long tail of dormant accounts that never log in. Those you handle at password reset, not by force.
### When a full migration is worth it
If you are already writing the lazy-rehash path, that is the cheapest moment you will ever get to change the algorithm underneath it. The 72-byte ceiling does not exist in Argon2id, and the [in-depth comparison of Argon2id and bcrypt](/blog/bcrypt-vs-argon2-vs-scrypt-password-hashing) covers when the switch pays for itself and when staying on bcrypt is the right call. The [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) is the reference to check your parameters against.
Do not start a migration solely because of this error. If your passwords are comfortably under 72 bytes, bcrypt remains a sound choice and section 6 already fixed your problem.
## FAQ
### Why does bcrypt say my password is longer than 72 bytes when it is short?
Because the bcrypt error is about passlib's internal probe, not your password. On the first call, passlib runs `detect_wrap_bug` with a fixed 255-byte test string. bcrypt 5.0.0 raises `ValueError` for anything over 72 bytes, so the probe fails and the error surfaces at your call site. A 14-byte password triggers it.
### Does bcrypt really ignore everything after 72 bytes?
Yes — bcrypt ignores every byte after 72, completely. Two 82-byte passwords sharing their first 72 bytes produce the identical hash `$2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S`, and each verifies against the other's hash. The boundary is exact: a difference at byte 72 changes the hash, a difference at byte 73 does not.
### Is the 72-byte limit a security problem?
bcrypt's 72-byte limit is a problem for long passphrases. Anyone who knows the first 72 bytes can append arbitrary bytes and authenticate, so every byte past the limit adds nothing. For passwords under 72 bytes it changes nothing at all. Pre-hashing with SHA-256 removes the exposure if long inputs must count fully.
### How many characters is 72 bytes?
72 bytes is 72 ASCII letters, but it depends on encoding: 36 Cyrillic or umlaut characters, 24 Chinese Han characters, 24 Japanese kana, or 18 emoji. bcrypt counts UTF-8 bytes rather than characters, so measure with `len(pw.encode("utf-8"))` in Python or `Buffer.byteLength(pw, "utf8")` in Node.
### Does patching `__about__` fix the passlib error?
No. Patching `__about__` does not fix the passlib bcrypt error: we applied the patch before `import passlib` in a clean process and the `ValueError` still fired. bcrypt 4.3.0 also lacks `__about__` and works fine with passlib, which proves the missing attribute is not the cause. The patch only silences the `(trapped) error reading bcrypt version` warning.
### Should I downgrade bcrypt to below 5.0?
As a stopgap, yes: bcrypt 4.3.0 with passlib 1.7.4 works. But 4.x silently truncates anything past 72 bytes, which is the behaviour 5.0 was released to stop, so treat the pin as temporary and move to calling bcrypt directly.
### Can I just truncate the password to 72 bytes myself?
Do not truncate a password to 72 bytes yourself. `pw[:72]` recreates the collision described above inside your own code, silently, with no library warning to catch it. Either pre-hash with SHA-256 and base64 so long inputs stay distinct, or validate the byte length up front and reject with a clear error message.
### What happens to passwords already hashed before I fixed this?
Existing bcrypt hashes keep verifying, because your verify path truncates the same way the hash path did. Only accounts registered with over-72-byte passwords are weakened, and you cannot recompute them without plaintext. Re-hash lazily on the next successful login, and handle dormant accounts at password reset.
---
### bcrypt vs Argon2 vs scrypt: password hashing in 2026
URL: https://go-tools.org/blog/bcrypt-vs-argon2-vs-scrypt-password-hashing
Compare bcrypt, Argon2id, and scrypt against OWASP 2026 parameters, with a decision guide and code samples for picking a password hash.
# bcrypt vs Argon2 vs scrypt: Password Hashing in 2026
**Short answer:** for any new project in 2026, use **Argon2id** with `m=19456, t=2, p=1`. That matches the [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) baseline, and it gives you the best GPU and side-channel resistance you can ship today.
If Argon2 isn't in your stack (rare, but it happens on some embedded or older runtimes), pick **scrypt** with `N=2^17, r=8, p=1`. Use **bcrypt** with `cost=12` only when you're stuck with a legacy system that already speaks bcrypt and you can't add a new dependency. Stick to **PBKDF2-HMAC-SHA-256 with 600,000 iterations** when FIPS-140 compliance is mandatory.
| Algorithm | OWASP 2026 parameters | When to pick |
|-----------|----------------------|--------------|
| Argon2id | `m=19456 KiB, t=2, p=1` | Default for new projects |
| scrypt | `N=2^17, r=8, p=1` | Argon2 not available |
| bcrypt | `cost=12` (min 10) | Legacy systems only |
| PBKDF2 | HMAC-SHA-256, 600k iterations | FIPS-140 required |
The rest of this article explains why these numbers, how to tune them for your hardware, and how to migrate without forcing a password reset. If you need strong test passwords for benchmarking, use the [random password generator](/tools/random-password-generator). For the broader picture, see the [web security best practices guide](/blog/security-best-practices).
## Why password hashing is different from general hashing
Hash functions look the same from the outside: data goes in, a fixed-length digest comes out, and you can't reverse it. But the design goals for "hash this 4 GB ISO" and "hash this 12-character password" pull in opposite directions. One should run as fast as silicon allows. The other should run as slow as your login latency budget tolerates.
Mixing them up is how breaches turn into account takeovers.
### Why MD5 and SHA-256 fall short for passwords
General-purpose hashes like MD5, SHA-1, and SHA-256 were built for throughput. They process gigabytes per second on commodity CPUs and tens of gigabytes per second on GPUs. That makes them excellent for file checksums and content addressing, and disastrous for passwords.
Hashcat benchmarks on a single RTX 4090 show roughly **164 GH/s for MD5** and **22 GH/s for SHA-256** in 2024. An eight-character lowercase-alphanumeric password (36^8 ≈ 2.8 × 10^12 candidates) falls to a single GPU in under a minute against MD5 and under a couple of minutes against SHA-256. A breached database storing `sha256(password)` is basically plaintext.
Salt won't save you either. It blocks pre-computed rainbow tables, but it does nothing to slow down a per-account attack: the attacker just hashes each candidate concatenated with the leaked salt.
For non-security checksums, MD5 and SHA-256 still pull their weight; that's what tools like the [general-purpose hash generator](/tools/md5-hash-generator) are built for. For a deeper comparison of when each algorithm is appropriate, read [MD5 vs SHA-256 hash algorithm comparison](/blog/md5-vs-sha256-hash-algorithm-comparison). But for passwords, you need a hash that runs slow on purpose.
### What a modern password hash needs to do
A password hash worth shipping in 2026 has three properties:
1. **Slow on purpose, with a tunable work factor.** Login should take 100–500 ms: fast enough that users don't notice, slow enough that an offline attacker burns days per million guesses. The work factor needs to be a parameter so you can crank it up as hardware improves.
2. **Per-record salt.** A unique random salt per password defeats rainbow tables and forces the attacker to attack each account on its own. Modern algorithms generate and embed the salt in the output string for you.
3. **Memory-hard.** GPUs and ASICs are fast at compute but expensive at high-bandwidth memory. An algorithm that requires tens of MiB per hash forces an attacker to provision RAM proportional to their parallelism, killing the cost-effectiveness of GPU farms.
bcrypt nails (1) and (2) but not (3). scrypt was the first algorithm to hit all three. Argon2 refined the design and won the Password Hashing Competition. The next section walks through each one.
## The three algorithms: architecture and tradeoffs
### bcrypt: Blowfish-based, time-hard
bcrypt was designed in 1999 by Niels Provos and David Mazières for OpenBSD. It's built on the Blowfish cipher, with an expensive key-setup phase ("EksBlowfish") repeated 2^cost times. The single tunable parameter is the **cost factor** (also called the "log rounds"): each increment doubles the work. A `cost=10` hash does 1,024 key schedules; `cost=14` does 16,384.
A bcrypt hash looks like this:
```
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
│ │ │ │
│ │ │ └─ 31-char base64 hash
│ │ └─ 22-char base64 salt
│ └─ cost factor (12)
└─ algorithm identifier ($2b$ = bcrypt v2)
```
The format is self-describing: `verify()` reads the cost and salt from the stored string, no separate columns required.
The downsides are real. bcrypt's memory footprint is about **4 KiB**, small enough that a high-end GPU can run thousands of bcrypt cores in parallel. And bcrypt **silently truncates input at 72 bytes**. A 100-character passphrase has the same security as its first 72 bytes. The maximum cost is 31, but anything above ~16 starts hurting login latency on commodity hardware.
### scrypt: the memory-hard pioneer
scrypt was published in 2009 by Colin Percival for the Tarsnap backup service and standardized as [RFC 7914](https://datatracker.ietf.org/doc/html/rfc7914) in 2016. It introduced the idea of **memory-hardness**: the algorithm fills a large buffer with pseudo-random data, then reads from random positions, forcing any implementation to actually allocate the memory.
scrypt takes three parameters:
- **N** — CPU/memory cost (must be a power of 2)
- **r** — block size in bytes (multiplier on memory and mixing rounds)
- **p** — parallelism (independent computations, mostly used to scale CPU time without scaling memory)
Memory usage is roughly `128 × N × r` bytes. With OWASP's recommended `N=2^17, r=8`, that's `128 × 131072 × 8 = 134,217,728` bytes, or **128 MiB per hash**.
scrypt also doubles as a key derivation function, not just a password hash. You'll find it in cryptocurrency wallets, full-disk encryption, and the original Litecoin proof-of-work. That dual role is convenient when you need both password storage and key derivation in one library.
### Argon2 (id/i/d): Password Hashing Competition winner
The Password Hashing Competition ran from 2013 to 2015, evaluating 24 candidate algorithms against memory-hardness, side-channel resistance, and implementation simplicity. Argon2 won. It was standardized as [RFC 9106](https://datatracker.ietf.org/doc/html/rfc9106) in 2021.
Argon2 has three variants. The differences come down to how the memory gets addressed during mixing:
- **Argon2d** uses data-dependent memory addresses. That gives the best resistance to GPU and ASIC attacks but leaks information through cache-timing side channels. Suitable for cryptocurrency proof-of-work, not authentication.
- **Argon2i** uses data-independent addresses. Side-channel safe, but slightly weaker against GPU tradeoff attacks.
- **Argon2id** is a hybrid: the first half of the first pass uses Argon2i indexing (side-channel safe), and the rest uses Argon2d indexing (GPU-resistant). RFC 9106 explicitly recommends Argon2id for password hashing, and so does OWASP.
Argon2 takes three parameters:
- **m** — memory in KiB
- **t** — time cost (number of passes over the memory buffer)
- **p** — parallelism (number of lanes processed concurrently)
An Argon2id hash uses the PHC string format and looks like this:
```
$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
```
Like bcrypt, all parameters live inside the string, so `verify()` doesn't need a parameter table.
## OWASP 2026 recommended parameters
The [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) is the canonical reference. The numbers below match its current guidance. They're conservative, sized for a typical web server with a 100–500 ms login latency budget, and you should still benchmark on your own hardware before shipping.
### Argon2id parameters: first choice
OWASP's baseline recommendation: **`m=19456 (19 MiB), t=2, p=1`**.
If your server has more RAM headroom, you can shift the work between memory and time. RFC 9106 publishes equivalent profiles; OWASP recommends any of these:
| memoryCost (m) | timeCost (t) | parallelism (p) | RAM per hash |
|----------------|--------------|-----------------|--------------|
| 47104 | 1 | 1 | 46 MiB |
| 19456 | 2 | 1 | 19 MiB (baseline) |
| 12288 | 3 | 1 | 12 MiB |
| 9216 | 4 | 1 | 9 MiB |
| 7168 | 5 | 1 | 7 MiB |
**Tuning rule of thumb.** Pick `m` first based on your peak concurrent-login RAM budget. If you expect 100 simultaneous logins and have 4 GiB to spare, that's 40 MiB per hash. Then increase `t` until a single verify takes 100–500 ms on your production CPU. Leave `p=1` unless you have a specific multi-core reason to change it (most web frameworks already give each request its own thread).
### scrypt parameters: when Argon2 isn't available
OWASP's recommendation: **`N=2^17 (131072), r=8, p=1`**, which uses 128 MiB per hash.
If 128 MiB per concurrent login is too much for your server, OWASP allows weaker profiles:
| N | r | p | RAM per hash |
|-----------|---|---|--------------|
| 2^17 | 8 | 1 | 128 MiB (preferred) |
| 2^16 | 8 | 1 | 64 MiB |
| 2^15 | 8 | 1 | 32 MiB |
`N` must be a power of two. Increasing `r` raises both memory and CPU work proportionally; increasing `p` raises CPU work without raising per-instance memory. For password hashing, leave `r` and `p` at the defaults and only tune `N`.
### bcrypt: cost factor 10+ for legacy only
OWASP no longer recommends bcrypt for new projects, but it's still everywhere: Devise, Spring Security, ASP.NET Identity, and countless homegrown auth systems default to it.
If you're stuck with bcrypt, the rules are:
- **Minimum bcrypt cost factor: 10.** Below 10, a single GPU finishes a leaked database in days.
- **Recommended: 12 to 14**, depending on hardware. On a modern x86 server, `cost=12` takes around 250 ms per hash; `cost=13` takes 500 ms.
- Target **100–300 ms per verify** on your production hardware. Benchmark, don't guess.
- Remember the **72-byte input limit**. If users can choose passphrases, pre-hash with SHA-256 (see the FAQ).
bcrypt's GPU resistance is bounded by its 4 KiB memory footprint. No bcrypt cost factor will ever match Argon2id's memory-hardness, so pick Argon2id when you can.
For a practical reference, on a 2024 EPYC server, `bcrypt(cost=12)` runs in roughly 250 ms; on a high-end laptop, closer to 350 ms. If your numbers fall outside 100–500 ms by an order of magnitude, recheck whether your library is actually doing native bcrypt or falling back to a slow JavaScript polyfill (some bundlers strip native dependencies in serverless builds).
### PBKDF2: FIPS-140 compliance path
PBKDF2 (RFC 8018) is the algorithm of last resort in security guidance. It's older than bcrypt, it isn't memory-hard, and it falls to GPU attacks faster than any of the three above. But it's the only password-hashing primitive that's **FIPS-140 validated**, which matters for federal government, healthcare HIPAA, and certain financial deployments.
When you need PBKDF2, use:
- **HMAC-SHA-256** as the PRF (don't use SHA-1; don't use plain SHA-256 without HMAC)
- **600,000 iterations** minimum (OWASP 2026 baseline)
- **At least a 16-byte random salt per password**
If FIPS doesn't apply to you, prefer Argon2id. PBKDF2's fixed-output, fixed-memory design means every dollar of GPU silicon an attacker buys translates directly into more password guesses per second.
NIST's [SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html) calls PBKDF2-HMAC "approved" for password hashing but stops short of recommending it over memory-hard alternatives. Read that as: NIST permits PBKDF2 because retiring it would invalidate every legacy government deployment, not because it's the best choice for a greenfield project.
## Decision framework: which algorithm should you pick?
### Comparison table
| Dimension | bcrypt | scrypt | Argon2id | PBKDF2 |
|-----------|--------|--------|----------|--------|
| Memory-hard | No | Yes | Yes | No |
| GPU resistance | Medium | High | Very high | Low |
| Side-channel resistance | Medium | Medium | High (id) | Medium |
| Parameter complexity | 1 (cost) | 3 (N, r, p) | 3 (m, t, p) | 1 (iterations) |
| Library maturity | Excellent | Good | Good | Excellent |
| Input length limit | 72 bytes | None | None | None |
| Standardization | de facto | RFC 7914 | RFC 9106 | RFC 8018 |
| OWASP 2026 status | Legacy only | Alternative | **First choice** | FIPS only |
### Use Argon2id by default
For a new project (typical web app, modern Node/Python/Go/Rust/JVM stack, no FIPS constraint), **use Argon2id with `m=19456, t=2, p=1`**. You get the best GPU and side-channel resistance available today, an embedded-parameter format that survives library upgrades, and no 72-byte input cap. The library ecosystem is mature: `argon2` on npm, `argon2-cffi` on PyPI, `golang.org/x/crypto/argon2`, the `argon2` crate on crates.io, all maintained and benchmarked.
### When to pick scrypt or bcrypt instead
**Pick scrypt when** Argon2 isn't available in your runtime (genuinely rare in 2026; even Cloudflare Workers and Deno have it now), or when you already have a scrypt-based system in production and the migration cost outweighs the security delta. scrypt is still a solid algorithm; it just lacks the side-channel polish of Argon2id.
**Pick bcrypt when** you're maintaining a legacy system, you have a hard dependency-minimization requirement (no native code, no extra packages), and the 72-byte input limit is acceptable for your user base. bcrypt has run at internet scale for two decades; its failure modes are documented.
**Pick PBKDF2 when** the regulator says so. That's the only reason. If your auditor accepts Argon2id (which a growing number now do for non-FIPS workloads), use Argon2id.
### Common mistakes to avoid
Most password-storage breaches in the last decade trace back to a handful of recurring engineering mistakes. None of them are exotic, and all of them get caught by reviewing your auth code with the list below in front of you.
- **Hashing passwords with raw SHA-256 or MD5.** This is the single biggest password-storage failure. See [MD5 vs SHA-256](/blog/md5-vs-sha256-hash-algorithm-comparison) for why these are wrong for passwords.
- **Reusing a single global salt across all users.** A salt has to be unique per record. Argon2 and bcrypt generate one for you; don't override that.
- **Setting hash time below 50 ms.** You traded security for a speed gain no user can perceive. Aim for 100–500 ms.
- **Setting hash time above 1 second.** You created a denial-of-service vector against your own login endpoint. Cap at ~500 ms.
- **Hashing passwords client-side and sending the digest to the server.** The hash is now the password. Anyone who steals the database can authenticate without ever inverting it. Always hash on the server.
- **Storing the algorithm parameters in a separate column.** The PHC string format puts them in the hash for you. Use it.
- **Logging passwords or hashes during error handling.** Both belong to the user, not your log aggregator. Scrub them at the request-parsing layer before they reach any logger.
- **Treating `verify()` exceptions as authentication failures.** A library that throws on a malformed stored hash should surface the error, not silently fall through to "wrong password." Distinguish between "wrong password" (return 401) and "stored hash is corrupt" (return 500 and page on-call).
## Real-world implementation
### Argon2id in Node.js
The `argon2` package (native bindings to the reference implementation) is the canonical choice on Node:
```js
import argon2 from 'argon2';
// Hashing on signup or password change
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2,
parallelism: 1,
});
// → '$argon2id$v=19$m=19456,t=2,p=1$$'
// Verifying on login
const ok = await argon2.verify(hash, candidate);
if (!ok) throw new Error('Invalid credentials');
// Detect outdated parameters and re-hash on successful login
if (argon2.needsRehash(hash, { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 })) {
const upgraded = await argon2.hash(candidate, {
type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1,
});
await db.users.update({ id: user.id }, { password_hash: upgraded });
}
```
The `needsRehash` step is what makes long-term migration painless: every successful login becomes an opportunity to upgrade the stored hash to current parameters, without bothering the user.
The same pattern in Python with `argon2-cffi`:
```python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(memory_cost=19456, time_cost=2, parallelism=1)
# Hash
stored = ph.hash(password)
# Verify
try:
ph.verify(stored, candidate)
except VerifyMismatchError:
raise ValueError('Invalid credentials')
# Re-hash on parameter upgrade
if ph.check_needs_rehash(stored):
stored = ph.hash(candidate)
```
In Go with `golang.org/x/crypto/argon2`:
```go
import (
"crypto/rand"
"golang.org/x/crypto/argon2"
)
func hashPassword(password string) ([]byte, []byte) {
salt := make([]byte, 16)
rand.Read(salt)
hash := argon2.IDKey([]byte(password), salt, 2, 19456, 1, 32)
return hash, salt
}
```
The Go standard library doesn't ship a PHC-format encoder; if you use the `argon2.IDKey` primitive directly, you have to encode the parameters and salt alongside the hash yourself. Most Go projects use a wrapper like `github.com/alexedwards/argon2id` for that.
Rust with the `argon2` crate is similarly idiomatic:
```rust
use argon2::{Argon2, PasswordHasher, PasswordVerifier, password_hash::{SaltString, rand_core::OsRng}};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default(); // Argon2id, m=19456, t=2, p=1 by default
let hash = argon2.hash_password(password.as_bytes(), &salt)?.to_string();
// On verify
let parsed = argon2::password_hash::PasswordHash::new(&hash)?;
argon2.verify_password(candidate.as_bytes(), &parsed)?;
```
In all three runtimes, the produced string is interchangeable: a hash created in Node verifies cleanly in Python or Rust. That cross-runtime compatibility makes Argon2 a safer bet for polyglot architectures than algorithm-specific wrappers.
### bcrypt-to-Argon2id migration pattern
You almost never get to wipe the user table and start over. The pattern that actually works is the one used in the [MD5-to-bcrypt section of our hash generator FAQ](/tools/md5-hash-generator): a soft, login-driven upgrade.
Add a column to track the algorithm:
```sql
ALTER TABLE users ADD COLUMN password_algo VARCHAR(16) NOT NULL DEFAULT 'bcrypt';
```
On login, dispatch to the right verifier:
```js
async function verifyAndMaybeRehash(user, candidate) {
let ok;
if (user.password_algo === 'argon2id') {
ok = await argon2.verify(user.password_hash, candidate);
} else if (user.password_algo === 'bcrypt') {
ok = await bcrypt.compare(candidate, user.password_hash);
if (ok) {
// Successful legacy verify → re-hash with Argon2id
const newHash = await argon2.hash(candidate, {
type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1,
});
await db.users.update({ id: user.id }, {
password_hash: newHash,
password_algo: 'argon2id',
});
}
}
return ok;
}
```
Set a sunset window of **6–12 months**. Send a "your password is stored using an outdated method, please log in to upgrade" email at the 9-month mark. After 12 months, accounts still on bcrypt require a forced password reset on next login. Active users migrate transparently; inactive accounts get a one-time friction event.
The same pattern works for migrating off scrypt or PBKDF2. The only state you need is the `password_algo` column.
### Pepper, length limits, and encoding pitfalls
A few sharp edges that bite real deployments:
**Pepper.** A pepper is an application-level secret added to every password before hashing, stored separately from the database (in a KMS, env var, or Hashicorp Vault). If your database leaks but your app secret doesn't, the leaked hashes are unattackable without the pepper. Apply it as an HMAC, not concatenation:
```js
import { createHmac } from 'crypto';
const peppered = createHmac('sha256', process.env.PEPPER).update(password).digest();
const hash = await argon2.hash(peppered, { type: argon2.argon2id, /* ... */ });
```
Rotate the pepper rarely (it requires re-hashing) but do support rotation by versioning it: `PEPPER_V2`, with a fallback to `PEPPER_V1` on verify.
**bcrypt 72-byte limit.** If you must use bcrypt and want to support arbitrary-length passwords, pre-hash with SHA-256 and base64-encode (avoiding embedded NUL bytes that bcrypt also handles inconsistently):
```js
import { createHash } from 'crypto';
const prepped = createHash('sha256').update(password, 'utf8').digest('base64');
const hash = await bcrypt.hash(prepped, 12);
```
The same `prepped` transformation must run on verify. Document this in your auth code with a giant comment so the next person to touch it knows what's happening.
**UTF-8 normalization.** The string `"café"` can be encoded as either `c-a-f-é` (4 codepoints, NFC) or `c-a-f-e + combining acute` (5 codepoints, NFD). They look identical but produce different hashes. Always normalize to NFC before hashing:
```js
const normalized = password.normalize('NFC');
```
This bites mobile keyboards and copy-paste from PDFs more often than you'd expect.
**Never pre-hash on the client.** A client-computed hash sent to the server is the new password. Anyone who reads your database can authenticate. Hash on the server, period. JWTs don't change this; see [how to decode JWT tokens](/blog/how-to-decode-jwt-token-guide) for what JWTs do and don't authenticate.
**Benchmark on production hardware, not your laptop.** A 13th-gen Intel laptop running Argon2id at `m=19456, t=2, p=1` finishes in roughly 35 ms. The same parameters on a `t3.small` EC2 instance take closer to 180 ms; on a Raspberry Pi 4, over 600 ms. Pick the hardware that will actually run production, time 1,000 verifies, and tune from the median. Login latency variance from cold-start serverless containers is also worth measuring; Lambda cold starts can add 200–800 ms unrelated to hashing.
## FAQ
### What's the difference between password hashing and encryption?
Hashing is one-way: you compute a fixed-length fingerprint that can't be reversed to recover the input. Encryption is two-way: with the right key, you can decrypt back to the original. Passwords must be hashed, not encrypted. A server shouldn't be able to recover any user's password, so that a database leak doesn't turn into a credential leak.
### Why can't I just use SHA-256 for passwords?
SHA-256 is built for speed. A modern GPU computes 22 billion SHA-256 hashes per second, so an 8-character lowercase password from a leaked database falls in minutes. Password hashes need three properties SHA-256 lacks: slow execution on purpose, per-record salt, and memory-hardness. The tradeoff principle is the same one explained in our [hash generator's "Don't Use MD5 for Security" guidance](/tools/md5-hash-generator), and you can read more about how attackers turn weak hashes into plaintext in [password entropy explained](/blog/password-entropy-explained).
### Is bcrypt still secure in 2026?
bcrypt itself hasn't been broken. The Blowfish-based key schedule remains cryptographically sound. What has changed is the threat model: GPUs and ASICs make bcrypt's lack of memory-hardness a meaningful weakness compared to Argon2id. OWASP's 2026 stance is that bcrypt is acceptable for legacy systems with cost ≥ 10, but new projects should pick Argon2id.
### Argon2i vs Argon2d vs Argon2id: which should I use?
Use **Argon2id**. RFC 9106 specifies it as the recommended variant for password hashing. Argon2i is data-independent (side-channel safe but weaker against GPU tradeoff attacks). Argon2d is data-dependent (GPU-strong but vulnerable to cache-timing side channels). Argon2id is a hybrid that gets both properties for the price of one.
### How do I choose Argon2id parameters for my app?
Start with the OWASP baseline: `m=19456, t=2, p=1`. Then benchmark on your production CPU and adjust:
1. Decide your per-login RAM budget (say, 50 MiB at peak concurrency).
2. Set `m` to that value or below.
3. Run `argon2.hash()` in a loop and measure wall time.
4. Raise `t` until the median sits between 100 and 500 ms.
Leave `p=1` unless you've profiled and know multi-lane parallelism helps your runtime. For high-traffic auth servers, biasing toward higher `t` and lower `m` often gives better RAM headroom.
### What's bcrypt's 72-byte limit and how do I handle long passphrases?
bcrypt feeds its input into the Blowfish key schedule, which truncates at 72 bytes. A 150-character passphrase has the same security as its first 72 bytes; the rest is ignored. The fix is to pre-hash with SHA-256 (32 bytes) or SHA-512 (64 bytes), base64-encode the digest to avoid NUL bytes, and feed that to bcrypt. Argon2id and scrypt have no such limit; they accept arbitrarily long input directly.
### Can I migrate bcrypt to Argon2 without forcing password resets?
Yes. The pattern is: store both algorithms behind a `password_algo` column, dispatch verification to the right library, and on every successful bcrypt verify, immediately re-hash with Argon2id and update the row. Active users migrate silently within their normal login cadence. Set a 6–12 month sunset window for inactive accounts, then force a password reset for any record still on bcrypt. The same pattern works for any algorithm-to-algorithm migration.
### Is PBKDF2 still a good choice in 2026?
Only when FIPS-140 compliance forces your hand: typical in federal government, regulated healthcare (HIPAA), and certain financial systems. Use HMAC-SHA-256 as the PRF with at least 600,000 iterations. PBKDF2 isn't memory-hard, so it falls to GPU attacks faster than Argon2id at equivalent latency budgets. If FIPS doesn't apply, pick Argon2id and skip the extra compliance work.
---
The 2026 password hashing answer is short: default to Argon2id with OWASP's baseline parameters, fall back to scrypt if Argon2 isn't available, keep bcrypt only where legacy demands it, and reserve PBKDF2 for FIPS-bound systems. Pair the hash with a per-record salt (every modern library handles this automatically), an application-level pepper stored outside the database, and a login-driven re-hash loop that lets you raise work factors as hardware improves.
Generate a representative password set with the [random password generator](/tools/random-password-generator), benchmark your verify path against your production CPU, and write the parameters into a constants file so the next engineer knows exactly what to bump in 2028. The full security context (TLS, session management, rate limiting, MFA) lives in our [web security best practices guide](/blog/security-best-practices).
---
### Bitwise Operations Explained: AND, OR, XOR, Shifts, and Masks
URL: https://go-tools.org/blog/bitwise-operations-complete-guide
Master bitwise operations with hands-on examples: AND, OR, XOR, shifts, two's complement, bitmasks, and feature flags, with code in JS, Python, Go, and C.
# Bitwise Operations in Practice: AND, OR, XOR, Shifts, Masks
You open a legacy PostgreSQL migration and see `permissions & 0b100`. A colleague ships a feature flag system that packs 32 booleans into a single integer. A Kubernetes subnet calc spits out `192.168.1.0/24` and you need to extract the network address in code. Three situations, one underlying skill: bitwise operations.
Most application-layer developers never need to reach for `&` or `^` in a web app, until suddenly they do. This guide walks through the six bitwise operators, two's complement, nine patterns worth memorizing, and the language-specific traps that will bite you (especially in JavaScript). Code is in JS, Python, Go, and C, and every example is runnable.
Open our [Base Converter](/tools/base-converter) in another tab. Several sections invite you to type in a number and watch the bit pattern change.
## Why bitwise operations still matter in 2026
High-level languages have not made bitwise operations obsolete. They have just hidden where the operations happen. A few places you are relying on them today, whether you realize it or not:
- PostgreSQL row-level security uses a bitmap of ACL privileges (`SELECT`, `INSERT`, `UPDATE`, `DELETE`, ...) packed into an integer.
- Linux capabilities replace the old root-or-nothing model with 40+ permission bits you combine with `|`.
- JWT algorithm headers encode the hash algorithm in a small field where bit-level comparison is common at the library layer.
- Snowflake, ULID, and UUIDv7 pack timestamp, machine ID, and sequence number into a single 64-bit or 128-bit integer using left shifts.
- Redis `BITCOUNT` and `BITOP` expose bitwise primitives directly to application code for cardinality estimation and A/B bucketing.
- Image processing reads 32-bit RGBA pixels and extracts channels with `&` and `>>`.
Bitwise operations remain O(1) at the CPU instruction level. When you pack 32 booleans into one integer, you save 31 bytes of memory, and (more importantly) you can check "any of these 32 flags set" in a single `!= 0` test.
## Binary foundations you need first
This guide assumes you already know how binary works. If you need a refresher, read our [Number Base Conversion Guide](/blog/number-base-conversion-binary-hex-octal-guide) first and come back.
A quick vocabulary check before we start:
- A bit is a 0 or a 1.
- A nibble is 4 bits (one hex digit).
- A byte is 8 bits.
- A word is typically 32 or 64 bits, depending on your CPU.
Integers in most languages come in fixed widths: 8, 16, 32, 64. The width matters a lot for bitwise operations because shifts can push bits off the end, and the sign bit sits at the leftmost position of signed integers.
Try this now. Open the [Base Converter](/tools/base-converter), enter `170` as decimal, and look at the binary output. You should see `10101010`, an alternating pattern we will come back to several times below.
## The six bitwise operators
Every mainstream language gives you the same six operators, sometimes with slight syntax differences. The symbols `&`, `|`, `^`, `~`, `<<`, `>>` work in JavaScript, Python, Go, Rust, C, C++, Java, and C# unchanged. JavaScript adds one extra: `>>>`, the unsigned right shift.
### AND (`&`): bit filter
The output bit is 1 only if both input bits are 1.
| A | B | A & B |
|---|---|-------|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
Think of AND as a gate: only bits that are set in *both* operands survive. The most common use is masking, keeping some bits and zeroing others.
```javascript
// Extract the low 4 bits (the rightmost nibble)
const value = 0b11010110; // 214
const low4 = value & 0x0F; // 0b00000110 = 6
// Check if a number is odd
const isOdd = (n) => (n & 1) === 1;
isOdd(7); // true
isOdd(42); // false
```
```python
# Same in Python
value = 0b11010110
low4 = value & 0x0F # 6
def is_odd(n):
return (n & 1) == 1
```
### OR (`|`): bit setter
The output bit is 1 if *either* input bit is 1.
| A | B | A \| B |
|---|---|--------|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
OR combines flags. If you have `READ = 1`, `WRITE = 2`, `EXECUTE = 4`, then `READ | WRITE` is `3`, with both permissions enabled.
```javascript
const READ = 0b001;
const WRITE = 0b010;
const EXEC = 0b100;
const rw = READ | WRITE; // 0b011 = 3
```
```python
READ, WRITE, EXEC = 0b001, 0b010, 0b100
rw = READ | WRITE # 3
```
### XOR (`^`): bit toggle
The output bit is 1 if the input bits *differ*.
| A | B | A ^ B |
|---|---|-------|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
XOR has three algebraic properties that power some of the cleverest tricks in computer science:
- `a ^ a = 0`: anything XOR'd with itself cancels.
- `a ^ 0 = a`: XOR with zero is the identity.
- `a ^ b ^ a = b`: XOR is its own inverse.
The last property is why XOR shows up in parity checks, stream ciphers, and the notorious "find the single non-duplicated number in an array" interview question. CRC checksums are XOR and shifts all the way down; see [why the same bytes give four different CRC-16 results](/blog/crc16-variants-modbus-ccitt-xmodem-guide) for the parameters that separate the variants. To actually compute one, the [CRC calculator](/tools/crc-calculator) runs all 63 catalogued variants at once.
```javascript
// Find the one unique number in an array where every other number appears twice
const findUnique = (arr) => arr.reduce((a, b) => a ^ b, 0);
findUnique([4, 1, 2, 1, 2]); // 4
```
```python
from functools import reduce
from operator import xor
find_unique = lambda arr: reduce(xor, arr, 0)
find_unique([4, 1, 2, 1, 2]) # 4
```
### NOT (`~`): bit inverter
Unary `~` flips every bit: 0 becomes 1, 1 becomes 0.
```javascript
~0b00001111 // -16 (JavaScript coerces to 32-bit signed)
~5 // -6
```
```python
~5 # -6
```
```go
// Go uses ^ as unary bitwise NOT, watch out
var x int8 = 5
fmt.Println(^x) // -6
```
The result of `~5` is `-6` in every mainstream language, and this surprises beginners. The reason is two's complement, which we cover in the next section. For now, just know that `~x` equals `-(x + 1)` in any language that uses two's complement for negatives (which is all of them).
### Left shift (`<<`): power-of-two multiplier
`x << n` shifts every bit of `x` to the left by `n` positions, filling zeros on the right. Mathematically, this multiplies by 2ⁿ.
```javascript
1 << 0 // 1 (2^0)
1 << 1 // 2 (2^1)
1 << 3 // 8 (2^3)
1 << 10 // 1024 (2^10 = 1 KiB)
// Building bit flags
const FLAG_ADMIN = 1 << 0;
const FLAG_EDITOR = 1 << 1;
const FLAG_REVIEWER = 1 << 2;
```
The handy thing about `1 << n` is that it creates a number with a single bit set at position `n`. That bit becomes a flag.
Watch out for overflow. In JavaScript, `1 << 31` is `-2147483648` (not `2147483648`) because JavaScript bitwise operators work on 32-bit signed integers.
### Right shift (`>>` vs `>>>`): divide or padded?
Right shift moves bits to the right. The question is what fills the vacated leftmost positions.
- `>>` (arithmetic right shift) preserves the sign bit. Negative numbers stay negative.
- `>>>` (logical or unsigned right shift) fills with zeros. Only JavaScript has this as a dedicated operator.
```javascript
-8 >> 1 // -4 (sign bit preserved)
-8 >>> 1 // 2147483644 (sign bit treated as a data bit)
8 >> 1 // 4
8 >> 2 // 2
```
In C, whether `>>` is arithmetic or logical for signed types is implementation-defined. Most compilers do arithmetic, but do not rely on this without checking. Go requires shift amounts to be unsigned integers and treats signed and unsigned types explicitly. Python has no `>>>` because it has no fixed-width integers.
## Two's complement: how computers represent negatives
If bits are just 0 and 1, how do you encode `-5`? The answer the world settled on in the 1960s is two's complement, and every modern CPU uses it.
The naive approach (reserve one bit for the sign) has two problems. First, you end up with both `+0` and `-0`, which is awkward. Second, addition and subtraction circuits have to check the sign bit, making the hardware more complex. Two's complement solves both.
The rule is short:
1. Take the positive binary representation.
2. Flip every bit (that is the "one's complement").
3. Add 1.
Worked example, encoding `-5` in 8-bit two's complement:
```
5 in binary: 0000 0101
flip all bits: 1111 1010 (this is -6 in two's complement!)
add 1: 1111 1011 ← this is -5
```
Verify with our base converter: input `251` (decimal) into the [Base Converter](/tools/base-converter) with base 10, and the binary output is `11111011`. In an 8-bit signed context, `11111011` is `-5`. In an 8-bit unsigned context, the same bit pattern is `251`. The bits are identical; the interpretation differs.
This explains the earlier `~5 = -6` surprise. Bitwise NOT inverts bits, which gives you one's complement. Two's complement is one's complement plus 1. So:
```
~x = -(x + 1) // identity in any two's complement language
~5 = -6
~(-3) = 2
```
For n-bit signed integers, the representable range is `-2ⁿ⁻¹` to `2ⁿ⁻¹ − 1`. An 8-bit signed integer covers `-128` to `127`. A 32-bit signed integer covers roughly `-2.1 billion` to `+2.1 billion`.
## Essential bit manipulation patterns
These nine patterns cover maybe 95% of the bit manipulation you will ever write. Memorize them and you will recognize them everywhere in systems code.
### Set a bit: `x | (1 << n)`
Turn bit `n` on, leave other bits unchanged.
```javascript
let flags = 0b0100;
flags = flags | (1 << 0); // 0b0101
```
### Clear a bit: `x & ~(1 << n)`
Turn bit `n` off, leave other bits unchanged. `~(1 << n)` is a mask with every bit set *except* bit `n`.
```javascript
let flags = 0b0111;
flags = flags & ~(1 << 1); // 0b0101
```
### Toggle a bit: `x ^ (1 << n)`
Flip bit `n` regardless of its current state.
```javascript
let flags = 0b0100;
flags = flags ^ (1 << 2); // 0b0000
flags = flags ^ (1 << 2); // 0b0100 again
```
### Check a bit: `(x >> n) & 1`
Returns 1 if bit `n` is set, 0 otherwise. Equivalent form: `(x & (1 << n)) !== 0`.
```javascript
const flags = 0b0101;
const isBit2Set = (flags >> 2) & 1; // 1
```
### Isolate lowest set bit: `x & -x`
Produces a value with only the rightmost `1` bit of `x` kept. The trick works because `-x` in two's complement is `~x + 1`, which flips every bit up to and including the lowest set bit.
```javascript
const x = 0b10110100;
const lowest = x & -x; // 0b00000100 = 4
```
This is the core trick inside Fenwick trees (Binary Indexed Trees) for O(log n) prefix sums.
### Count set bits (popcount)
Counting the number of `1` bits in an integer. Most languages now have a native function:
```javascript
// JavaScript (BigInt or manual)
const popcount = (n) => {
let count = 0;
while (n) { count += n & 1; n >>>= 1; }
return count;
};
popcount(0b10110100); // 4
```
```python
# Python 3.10+
(0b10110100).bit_count() # 4
```
```go
// Go
import "math/bits"
bits.OnesCount(0b10110100) // 4
```
### XOR swap without a temp variable
A classic party trick: swap two integers without a third variable. Never use this in production (it is slower than a temp variable and breaks if `a` and `b` alias the same memory location), but it is worth understanding.
```javascript
let a = 5, b = 9;
a = a ^ b; // a = 5 ^ 9
b = a ^ b; // b = (5 ^ 9) ^ 9 = 5
a = a ^ b; // a = (5 ^ 9) ^ 5 = 9
// a = 9, b = 5
```
### Detect power of two: `(x & (x - 1)) === 0`
A power of two has exactly one bit set. Subtracting 1 flips that bit off and sets every lower bit. ANDing gives zero only for powers of two (and 0 itself, so guard with `x > 0`).
```javascript
const isPow2 = (x) => x > 0 && (x & (x - 1)) === 0;
isPow2(16); // true
isPow2(17); // false
```
### Fast oddness check: `x & 1`
Faster than `x % 2` in some languages, identical in others after compiler optimization. Worth it in hot loops or when readability does not matter.
```javascript
const isOdd = (x) => (x & 1) === 1;
```
## Bitmask flags in real code
The patterns above show up in production code every day. Here are four places you will meet them.
### Feature flags in 32 booleans
Instead of a 32-field struct of booleans, pack them into one integer:
```javascript
const FLAGS = {
DARK_MODE: 1 << 0,
NEW_NAV: 1 << 1,
AI_SUGGESTIONS: 1 << 2,
BETA_EDITOR: 1 << 3,
// ... up to 1 << 31
};
let userFlags = 0;
userFlags |= FLAGS.DARK_MODE | FLAGS.AI_SUGGESTIONS; // opt in
if (userFlags & FLAGS.AI_SUGGESTIONS) {
showSuggestions();
}
userFlags &= ~FLAGS.DARK_MODE; // opt out
```
This stores 32 booleans in 4 bytes and lets you query any subset with a single AND. Databases love this pattern because it is one column instead of 32.
### Unix file permissions
`chmod 755` is bitwise. The three octal digits map to three triples of bits:
```
7 = 111 (owner: rwx)
5 = 101 (group: r-x)
5 = 101 (others: r-x)
```
Try it: open the [Base Converter](/tools/base-converter), set source to octal, enter `755`, and look at the binary output `111101101`. That is literally how the filesystem stores the permission field.
Setting only "group write":
```javascript
const perms = 0o755;
const withGroupWrite = perms | 0o020; // 0o775
```
### IP subnet masking
Given `192.168.1.10/24`, extract the network address by ANDing with the mask:
```javascript
const ip = 0xC0A8010A; // 192.168.1.10
const mask = 0xFFFFFF00; // 255.255.255.0 (/24)
const network = ip & mask; // 0xC0A80100 = 192.168.1.0
```
If you would rather not do that masking by hand, the [subnet calculator](/tools/subnet-calculator) runs the same AND for you and reports the network address, broadcast address, and usable host range in one pass.
### Packed IDs: Snowflake
Twitter's Snowflake packs timestamp, machine ID, and sequence into a 64-bit integer:
```
┌─ 1 bit ─┬─── 41 bits ───┬─ 10 bits ─┬─ 12 bits ─┐
│ sign │ timestamp │ machine │ seq │
└─────────┴───────────────┴───────────┴───────────┘
```
Encoding an ID is two shifts and two ORs:
```javascript
const id = (BigInt(timestamp) << 22n) |
(BigInt(machineId) << 12n) |
BigInt(sequence);
```
Decoding is the reverse: right shift and mask. For a full walkthrough of when to pick Snowflake vs ULID vs UUIDv7, see our [distributed ID comparison](/blog/uuid-v4-v7-ulid-snowflake-id-comparison).
## Cross-language gotchas
### JavaScript: the 32-bit coercion trap
JavaScript converts operands to 32-bit signed integers before every bitwise operation, then converts the result back to a `Number`. Any value above `2³¹ − 1 = 2147483647` overflows:
```javascript
2147483647 | 0 // 2147483647 (still fine)
2147483648 | 0 // -2147483648 (overflowed!)
4294967295 | 0 // -1 (all bits set, interpreted signed)
```
For 64-bit work, use `BigInt`. It has independent bitwise operators with no width limit:
```javascript
(2n ** 40n) | 1n // 1099511627777n
```
### Operator precedence bugs
This is one of the most common real-world bitwise bugs:
```javascript
// Buggy: reads as (x & (1 == 0)) because == binds tighter than &
if (x & 1 == 0) { /* ... */ }
// Correct: parenthesize
if ((x & 1) == 0) { /* ... */ }
```
Comparison operators bind tighter than bitwise AND/OR/XOR in C, JavaScript, Python, Go, and most descendants. Parenthesize when in doubt.
### Language comparison table
| Language | Width coercion | Negative `>>` | BigInt support |
|----------|---------------|---------------|----------------|
| JavaScript | Forces 32-bit signed; `>>>` is unsigned | arithmetic | `BigInt` has separate operators |
| Python | Arbitrary precision; no fixed width | arithmetic | Native |
| Go | Strict; shift amount must be unsigned | arithmetic for signed types | `math/big` |
| C/C++ | Follows type; `int`, `unsigned`, etc. | implementation-defined for signed | None built in |
| Rust | Strict; panics on overflow in debug | arithmetic for signed types | `u128` / external crates |
### Python's infinite-width twist
Python integers have no fixed width, so two's complement logic extends "infinitely" to the left. That is why `~5` is `-6` (not `250` or `65530`): Python treats the result as a negative integer, not a fixed-width bit pattern. If you need wrap-around semantics, mask explicitly:
```python
# Simulate 8-bit NOT
(~5) & 0xFF # 250
```
## Performance reality check in 2026
The common lore is that bitwise operations are "always faster." In 2026, that is half true.
Compilers already do the obvious rewrites. Modern optimizers turn `x * 2` into `x << 1` automatically. Writing `x << 1` in application code for speed is cargo-cult performance tuning. It does not help, and it hurts readability.
Where bitwise code genuinely wins:
- Hot loops in numeric code: popcount, leading and trailing zero counts, bitboard chess engines.
- Compact data structures: Bloom filters, roaring bitmaps, Fenwick trees.
- Hardware registers and memory-mapped I/O: embedded code, kernels, firmware.
- Cryptography primitives: AES, ChaCha20, and SHA are all built from XOR, rotates, and shifts.
- Compression and decompression: Huffman coding, run-length, packed integers.
- Database engines: bitmap indexes, packed column formats like Parquet dictionary encoding.
Where it does not help: replacing `x % 2` with `x & 1` in a business-logic function that runs twice per request. The speedup is unmeasurable; the readability cost is real.
The one case where bit manipulation always wins is memory footprint. Packing 32 flags into an `int` saves 31 bytes compared to 32 booleans. At scale (millions of user records, billions of events) that is the difference between a cache-friendly layout and a workload that thrashes L2.
## Quick reference cheat sheet
| Operation | Operator | Example | Result | Typical Use |
|-----------|----------|---------|--------|-------------|
| AND | `&` | `0b1100 & 0b1010` | `0b1000` | Mask/extract bits |
| OR | `\|` | `0b1100 \| 0b1010` | `0b1110` | Combine flags |
| XOR | `^` | `0b1100 ^ 0b1010` | `0b0110` | Toggle / detect diff |
| NOT | `~` | `~0b1100` | `...11110011` | Invert for mask |
| Left shift | `<<` | `1 << 3` | `8` | Multiply by 2ⁿ |
| Right shift | `>>` | `16 >> 2` | `4` | Divide by 2ⁿ (signed) |
| Unsigned right shift (JS) | `>>>` | `-1 >>> 0` | `4294967295` | Treat as unsigned |
| Set bit `n` | `\|` | `x \| (1 << n)` | | Turn bit on |
| Clear bit `n` | `&` `~` | `x & ~(1 << n)` | | Turn bit off |
| Toggle bit `n` | `^` | `x ^ (1 << n)` | | Flip bit |
| Check bit `n` | `&` | `(x >> n) & 1` | `0` or `1` | Test bit |
| Lowest set bit | `&` `-` | `x & -x` | | Isolate bit |
| Is power of 2 | `&` | `x > 0 && (x & (x-1)) == 0` | bool | Test power |
## FAQ
### What's the difference between logical (`&&`) and bitwise (`&`) AND?
Logical AND works on whole boolean values and short-circuits, so `false && expr` never evaluates `expr`. Bitwise AND works on individual bits of integers and always evaluates both sides. Use `&&` for conditions, `&` for bit manipulation.
### Why does `~1` equal `-2` in most languages?
Bitwise NOT on `1` flips every bit to produce the one's complement. In two's complement integer representation, flipping all bits of `x` gives `-(x + 1)`, so `~1` equals `-2`, `~0` equals `-1`, and `~(-1)` equals `0`. This identity holds in JavaScript, Python, Go, C, Rust, and every other language that stores signed integers in two's complement.
### Is `x << 1` really faster than `x * 2`?
Not in practice. Every modern compiler recognizes `x * 2` and emits the same shift instruction at the machine level, so benchmarks show no measurable difference on x86 or ARM. Use `x * 2` for readability; reserve `<<` for cases where you are intentionally thinking in bits, such as building a bitmask or packing structured IDs.
### Does JavaScript support 64-bit bitwise operations?
JavaScript does not support 64-bit bitwise operations with the standard `&`, `|`, `^`, `<<`, `>>` operators, because those force operands to 32-bit signed integers before the operation runs. For 64-bit or larger values, use `BigInt` literals such as `1n << 40n`, which give arbitrary-precision bitwise operations with their own matching operators.
### How do I count the number of set bits efficiently?
Use your language's built-in: `bits.OnesCount` in Go, `Integer.bitCount` in Java, `.bit_count()` in Python 3.10+, `popcount` intrinsics in C/C++. These map to a single `POPCNT` CPU instruction on modern x86 and ARM.
### When should I use bitmask flags instead of a struct of booleans?
Use bitmask flags when you need to store many booleans compactly (databases, network protocols, file formats) or test combinations quickly with a single AND such as `flags & REQUIRED_MASK`. Prefer a struct of booleans when fields have different types, when you need descriptive debug output, or when readability matters more than a few bytes of memory.
### What happens when I shift by more than the bit width?
Undefined in C/C++. In JavaScript, the shift count is taken `mod 32`, so `1 << 32` is `1`, not `0`. In Python, there is no width, so `1 << 100` is just a larger integer. Never rely on overshift behavior; mask the shift count yourself if needed.
### Why does Python's `~5` give `-6` instead of `2`?
Python integers have no fixed width, so two's complement extends conceptually to infinity. `~5` equals `-(5 + 1) = -6`, same as every other two's complement language. If you want the 8-bit "inverted" value `250`, mask: `(~5) & 0xFF`.
### Is XOR encryption secure?
A one-time pad with a truly random key as long as the message is information-theoretically unbreakable. Reusing the same key across messages is catastrophically insecure, and standard XOR "encryption" with a short repeating key is trivially breakable. Real ciphers like AES and ChaCha20 use XOR internally, but as one step among many.
### How do I represent a negative number using two's complement by hand?
Write the positive value in binary at the target width, flip every bit, then add 1. Example: `-5` in 8 bits = `00000101` → flip to `11111010` → add 1 → `11111011`. Verify with our [Base Converter](/tools/base-converter) by converting `251` (the unsigned interpretation of `11111011`) and confirming you get `11111011`.
## Related tools and further reading
- [Base Converter](/tools/base-converter): type any number and watch the bits
- [Number Base Conversion Guide](/blog/number-base-conversion-binary-hex-octal-guide): prerequisite reading on binary, octal, and hex
- [UUID v4 vs v7 vs ULID vs Snowflake](/blog/uuid-v4-v7-ulid-snowflake-id-comparison): bit packing in distributed IDs
- [Security Best Practices](/blog/security-best-practices): permission bitmaps and their pitfalls
---
### Character & Word Limits 2026: Twitter, SMS, SEO, Instagram Guide
URL: https://go-tools.org/blog/character-limits-by-platform-guide
Character and word limits 2026 across Twitter, SMS GSM-7/UCS-2, SEO meta, Instagram, and LinkedIn — counting math plus live progress bars for 6 platforms.
# Character & Word Limits 2026: Twitter, SMS, SEO, Instagram Guide
A **character limit** is the maximum number of Unicode code points a platform accepts in a single field: 280 for a Twitter post, 160 for a single-segment SMS in GSM-7, around 160 for a Google meta description before truncation. The number you care about depends on where you publish and whether your text contains emoji, smart quotes, or CJK characters, all of which change the math.
This guide is for social-media writers, SEO specialists, marketing copywriters, SMS senders billed per segment, and developers writing validation that has to match what Twitter, Instagram, or SMS gateways actually count. Jump to the [quick reference table](#quick-reference-every-platforms-character-and-word-limit) for the 25-platform cheat sheet, or check your draft live against six major platforms in the [Word Counter](/tools/word-counter), where progress bars turn red the moment you cross a limit.
## Quick reference: every platform's character and word limit
The table below covers the 30+ fields writers and developers run into most often. "Hard limit" is the platform-enforced ceiling; "Visible / above the fold" is what readers see before a truncation point; "Sweet spot" is the empirical range where content performs best.
| Platform | Hard limit | Visible / above the fold | Sweet spot | Counts emoji as |
|---|---|---|---|---|
| Twitter / X post | 280 chars | 280 | 70-100 chars | 1 codepoint |
| Twitter / X bio | 160 chars | 160 | — | 1 codepoint |
| Twitter / X display name | 50 chars | 50 | — | 1 codepoint |
| X Premium long-form | 25,000 chars | — | — | 1 codepoint |
| Instagram caption | 2,200 chars | first 125 (then "more") | <125 for hook | 1 codepoint |
| Instagram bio | 150 chars | 150 | — | 1 codepoint |
| Instagram hashtags | max 30 | — | 5-10 | — |
| LinkedIn post | 3,000 chars | first 210 (then "see more") | <1,300 | 1 codepoint |
| LinkedIn article | 110,000 chars | — | — | 1 codepoint |
| LinkedIn headline | 220 chars | 220 | — | 1 codepoint |
| Facebook post | 63,206 chars | ~477 desktop / ~125 mobile | <80 for organic | 1 codepoint |
| TikTok caption | 2,200 chars | first ~100 | <150 | 1 codepoint |
| YouTube title | 100 chars | 70 (search) | <60 | 1 codepoint |
| YouTube description | 5,000 chars | first 100-150 above fold | first 150 for hook | 1 codepoint |
| YouTube comment | 10,000 chars | — | — | 1 codepoint |
| Reddit title | 300 chars | — | <60 (subreddit-dependent) | 1 codepoint |
| Reddit comment | 10,000 chars | — | — | 1 codepoint |
| Discord message | 2,000 chars | 2,000 | — | 1 codepoint |
| Discord embed description | 4,096 chars | — | — | 1 codepoint |
| Slack message | 40,000 chars | — | <2,000 for readability | 1 codepoint |
| Pinterest pin description | 500 chars | first 50-60 | <125 | 1 codepoint |
| Mastodon toot | 500 chars (configurable) | 500 | — | 1 codepoint |
| Bluesky post | 300 chars | 300 | — | 1 grapheme cluster |
| Threads post | 500 chars | 500 | — | 1 codepoint |
| SEO meta description (Google) | ~160 chars desktop / ~120 mobile | 150-160 | 150-160 | 1 codepoint |
| SEO page title (Google) | ~60 chars desktop / ~50 mobile | 50-60 | 50-60 | 1 codepoint |
| Open Graph description | ~200 chars before LinkedIn/FB clip | 150-200 | 150-200 | 1 codepoint |
| Twitter Card description | 200 chars max | 200 | 150-200 | 1 codepoint |
| SMS single segment (GSM-7) | 160 chars | — | — | special — see below |
| SMS single segment (UCS-2 / emoji) | 70 chars | — | — | 1 codepoint |
| WhatsApp message text | 65,536 chars | — | — | 1 codepoint |
| Email subject line | no platform limit | ~60 desktop / ~30 mobile | <50 | 1 codepoint |
| Google Ads headline | 30 chars × 15 headlines | 30 each | 30 | 1 codepoint |
| Google Ads description | 90 chars × 4 desc | 90 each | 90 | 1 codepoint |
| App Store title | 30 chars | 30 | 30 | 1 codepoint |
| App Store subtitle | 30 chars | 30 | 30 | 1 codepoint |
| App Store description | 4,000 chars | first 252 above fold | 252 hook | 1 codepoint |
| Play Store short description | 80 chars | 80 | 80 | 1 codepoint |
| Play Store long description | 4,000 chars | first 80 above fold | 80 hook | 1 codepoint |
Content above the "sweet spot" line tends to get truncated, downranked, or cropped off the visible card. X Premium long-form and Mastodon (configurable per instance) are the rare exceptions that let you write past 500 characters without penalty. Every count above, except where SMS rules apply, is a Unicode code-point count: one emoji costs 1 character, not 2. To verify a draft against the six most common limits at once, paste it into the [Word Counter](/tools/word-counter); the progress bars catch over-limit text before you hit publish.
## How characters are actually counted (Unicode code points vs UTF-16)
Three different tools can hand you three different character counts for the same string. "Character" is not a single thing: it could mean a Unicode code point, a UTF-16 code unit, or a grapheme cluster, and each platform picks one.
### What is a "character": codepoint vs code unit vs grapheme
A **codepoint** is a Unicode scalar value: any integer from U+0000 to U+10FFFF that Unicode has assigned to a character or marked as reserved. A **code unit** is the smallest piece of an encoding; UTF-16 uses 16-bit code units, UTF-8 uses 8-bit code units. A **grapheme cluster** is what humans perceive as a single visible character. Sometimes that means one codepoint, sometimes a base codepoint plus combining marks, sometimes a zero-width-joiner sequence like the family emoji 👨👩👧👦 (seven codepoints joined into one visible glyph).
For the string `"a🌍👨👩👧"` the three counts disagree:
| Counting method | Result | Used by |
|---|---|---|
| UTF-16 code units (JS `string.length`) | 10 | Naive JavaScript code |
| Unicode code points | 6 | Twitter, Instagram, SMS gateways |
| Grapheme clusters | 3 | Bluesky, screen readers, text editors |
### Why `string.length` lies about emoji
JavaScript stores strings as UTF-16 internally. Any codepoint above U+FFFF (every emoji, all astral-plane characters) is encoded as a surrogate pair: two 16-bit code units. The `.length` property reports those two units, not one character.
```javascript
"🌍".length // 2 (UTF-16 code units)
[..."🌍"].length // 1 (codepoints — what Twitter/SMS counts)
"🌍".match(/./gu).length // 1 (codepoints via regex with /u flag)
```
The spread operator and the `/u` regex flag both iterate by codepoint, which matches what Twitter, Instagram, and SMS gateways measure against their limits. A validation function that uses raw `.length` will reject tweets that are actually under the cap, or, worse, let through messages your downstream system will reject.
### What about CJK and combining marks
Chinese, Japanese, and Korean ideographs are each a single codepoint and count as one character on every platform. Where they get expensive is SMS: any non-GSM-7 character flips the whole message to UCS-2 encoding, dropping the segment limit from 160 to 70 (covered in the next section).
Combining marks behave differently. The accented `á` written as `á` is one codepoint; the same `á` written as `a` + `́` (combining acute accent) is two codepoints but one grapheme cluster. Most platforms count by codepoint, so the second form costs one extra character. Bluesky is the visible exception: it counts grapheme clusters, so both forms cost 1.
### Counting in different languages: quick reference
```javascript
// JavaScript
[...str].length // codepoints
Array.from(str).length // codepoints
// Python 3 — len() is codepoint by default
len(s)
// Go — utf8 package
utf8.RuneCountInString(s)
// Rust — chars() iterates codepoints
s.chars().count()
// Java — codePointCount
s.codePointCount(0, s.length())
```
For comparison, the [Base64 encoder](/tools/base64-decode-encode) reminds you of the other direction: when text is encoded to Base64 for transmission, every 3 bytes of UTF-8 input become 4 ASCII output characters, so the encoded length depends on the byte count, not the codepoint count. Paste a single emoji and watch the Base64 output expand to 8 characters; the same emoji that costs 1 character on Twitter takes 4 bytes in UTF-8.
To see codepoint counts (the number Twitter actually measures) on any draft, the [Word Counter](/tools/word-counter) is Unicode-correct by default.
## SMS character limit: GSM-7, UCS-2, and multi-part messages
SMS is the only major channel where adding a single emoji can literally double your bill. The reason is encoding, and the math has been the same since 1985.
### The 160-character magic number: GSM-7 history
The 1985 GSM-03.38 standard fixed an SMS payload at 140 bytes. With a 7-bit character encoding, 140 bytes hold 1,120 bits ÷ 7 = 160 characters. That's where the famous **sms character limit** of 160 comes from. The GSM-7 character set covers 128 base characters plus a 10-character extension (covering `{ } [ ] | \ ~ ^ €` and form feed). Inside that set you get the full 160-char budget per segment.
Characters that fall **outside** GSM-7 and force a switch:
- All emoji
- Curly / smart quotes (`"` `"` `'` `'`); note these are different from the ASCII straight quotes `"` `'`
- Most accented Latin letters beyond the 35 in GSM-7 (`é á ñ ü ø` etc.; GSM-7 includes only `ä ö å æ ø à è ì ò ù` and a few others)
- Full-width punctuation, CJK characters, Arabic, Hebrew, Greek lowercase, Cyrillic
- Backtick `` ` `` and tilde `~` (the tilde is in the GSM-7 extension table, so it costs 2 of your 160 chars)
### UCS-2 trap: one emoji drops you from 160 to 70
The moment a single non-GSM-7 character appears anywhere in the message, the entire message switches to UCS-2 encoding. UCS-2 uses 16 bits per character, so 140 bytes ÷ 2 = **70 characters per segment**. Some real examples:
```
"Hello, your code is 12345" → 26 chars, GSM-7, 1 segment
"Hello, your code is 12345 ✓" → 28 chars, GSM-7 (✓ in extension), 1 segment
"Hello, your code is 12345 ✅" → 28 chars, UCS-2 (emoji), 1 segment (under 70)
"Hello, "your" code is 12345 ✅" → smart quotes + emoji → UCS-2
"Hi 你好" → CJK → UCS-2, 1 segment (5 chars)
```
That last "Hi 你好" example is the gotcha: it's only 5 characters but it eats UCS-2 pricing and the next 65 characters you add will fit in one segment, then segment 2 starts.
### Multi-part SMS segments (concatenation)
Once you cross 160 (GSM-7) or 70 (UCS-2), the message splits into multiple segments. Each segment carries a 7-character User Data Header (UDH) used for reassembly, so the available payload per segment drops:
- GSM-7 multi-part: **153 characters per segment**
- UCS-2 multi-part: **67 characters per segment**
The receiving phone reassembles the segments invisibly to the recipient, but **billing is per segment**, not per message. A 161-character GSM-7 message costs 2 segments. A 1,000-character GSM-7 message costs 7 segments (153 × 6 = 918, 7th segment carries the last 82).
### Cost math: when one emoji doubles your bill
Take an 80-character plain-text marketing message:
- Plain text: 80 chars → GSM-7 → 1 segment at price X
- Add one emoji: 80 chars → UCS-2 → 80 > 70 → 2 segments at price 2X
Doubling the bill from one emoji is real and it scales. A campaign of 100,000 messages at $0.0075 per segment costs $750 in GSM-7 vs. $1,500 in UCS-2, a $750 emoji. Every major SMS provider (Twilio, Bandwidth, AWS SNS, MessageBird, Vonage) bills this way. The encoding rules are GSM standard, not vendor policy. The history of byte-level encoding tradeoffs, and why ASCII / UTF-8 / UCS-2 even exist as separate standards, is covered in [Understanding Base64](/blog/understanding-base64), which is the same family of "bits into characters" problem applied to email instead of SMS.
### How to keep messages in GSM-7
- Use ASCII straight quotes `"` `'`, not smart quotes
- Use ASCII hyphen `-`, not em-dash `—` or en-dash `–`
- Spell out `(c)` and `(R)`, not `©` and `®`
- Avoid emoji unless the campaign budget assumes UCS-2 cost
- Provider consoles (Twilio's, Bandwidth's, MessageBird's) show "encoding: GSM-7" or "UCS-2" next to the preview; verify before broadcast
The fastest sanity check during drafting is the [Word Counter](/tools/word-counter)'s SMS progress bar, which reports against the 160-char baseline. If your text triggers UCS-2, mentally divide your character count by 2.29 to estimate the segment count under the 70-char rule.
## SEO limits: meta description, title tag, OG, Twitter Card
SEO character limits are softer than platform limits (Google won't reject your page if a meta description hits 300 characters), but the practical truncation rules matter for click-through rate. The numbers below still apply in 2026.
### Meta description: 150-160 character sweet spot
Google's desktop search results truncate the meta description around 155-165 characters; mobile clips somewhere between 100 and 120. The exact truncation point varies because **Google measures display pixels, not characters**. A description full of `W` and `M` glyphs hits the truncation pixel earlier than one full of `i` and `l`.
Practical writing rules:
- Target 150-160 characters total
- Put core message in the first 120 characters (mobile-safe)
- Lead with the **meta description character limit** keyword for the page in the first 30 characters
- End with a CTA in the last 30 characters, readable even when desktop cuts the middle
The 2017-2018 era saw Google briefly expand meta description display to 320 characters, and a generation of SEO tutorials still cites that number. Google reverted to 160 in mid-2018. Writing past 200 characters today just hides the second half.
A different failure mode: descriptions under 120 characters often get replaced entirely. Google decides your description doesn't fully serve the query and pulls a different passage from the page body, so you lose CTR control without warning.
### Title tag: 60 desktop, 50 mobile
Title tags clip at roughly 60 characters on desktop and 50 on mobile. Same pixel-based truncation as descriptions, same caveat about wide glyphs.
Sweet spot: 50-60 characters, with the target keyword in the first 30 so it survives any clip. Long-tail brand suffixes (`| Brand Name`) belong at the end, where truncation is least painful.
### Pixel-width vs character-count: Google's actual rule
Google's SERP description container is roughly 920 pixels wide on desktop. Average character width sits around 6.5 pixels, yielding the 140-160 character empirical target. But the per-character spread is wide: `i` renders at about 3 pixels, `M` at about 11. A description of all-caps copy ("BEST WIDGETS FOR WINTER WEDDINGS") clips substantially earlier than a lowercase equivalent.
Pre-publish previews using pixel-accurate SERP simulators are more reliable than character counters for SEO copy.
### OG description and Twitter Card description
The Open Graph protocol's `og:description` is what Facebook, LinkedIn, Slack, and Discord render under a shared link preview. Display caps vary by platform: most clip around 200 characters, some extend to 300. The Twitter Card `twitter:description` is hard-capped at 200 characters in Twitter's parser.
Sensible defaults:
- 150-200 characters for both OG and Twitter Card
- They can match your meta description, but OG can run slightly longer because OG length doesn't affect search ranking
- Validate your structured-data choices (especially what gets pulled into OG by mistake) using the patterns in [Security Best Practices](/blog/security-best-practices), where untrusted OG metadata is a common phishing vector
### What "no character limit" actually means
H1 tags, body content, and URL slugs have no platform-enforced SEO character limit, but soft limits still apply:
- H1 > 70 characters breaks visual hierarchy and skim-ability
- URL slugs technically unlimited; Google displays around 90 characters in the SERP, anything beyond is cosmetic
- Body content has no length cap, but Google ranks helpful content over padding, so word count alone is not a ranking signal
The [Word Counter](/tools/word-counter) tracks both meta description (160) and title tag (60) live as you draft, with progress bars that turn amber and red as you approach the truncation pixel.
## Social platforms: Twitter/X, Instagram, LinkedIn, Facebook, and beyond
Each platform's character ceiling has a story behind it and a sweet spot below the hard limit where content actually performs.
### Twitter / X: 280, premium 25,000, URL substitution rule
The standard **twitter character limit** is 280 characters, doubled from 140 in November 2017. X Premium subscribers can post long-form content up to 25,000 characters with rich formatting, but the 280-char post is still the dominant form for organic reach.
The non-obvious rule is URL substitution. Twitter wraps every URL, no matter how long, in a 23-character `t.co` short link at publish time. The 23-character cost is fixed.
```
published_length = raw_length − URL_length + 23
```
Example: a draft like `"Check this: https://example.com/very-long-path?id=12345"` is 53 raw characters. The URL is 38 characters, so it gets replaced with a 23-char `t.co` link, and the published length is 53 − 38 + 23 = 38 characters. Save 15 characters you didn't know you had.
For pasting a long URL into a draft, the [URL encoder/decoder](/tools/url-decoder-encoder) is a quick way to verify what counts as a URL (Twitter recognizes URLs by RFC 3986 patterns, query strings and fragments included). Subdomains, schemes, ports, paths, queries, and fragments are all swallowed by the 23-character substitution.
Other Twitter fields: display name 50 chars, bio 160 chars, handle 15 chars. Threads (Meta's Twitter equivalent) uses a 500-character limit instead.
### Instagram: 2,200 caption, 30 hashtags, 125-char hook
Instagram captions allow 2,200 characters, but the feed only shows the first **125 characters** before collapsing the rest behind a "... more" tap. More than half of readers never tap. The **instagram caption limit** that matters for engagement is therefore 125, even though the hard limit is 2,200.
The 30-hashtag cap is hard, and attempting a 31st hashtag fails the post. The 5-10 hashtag range tends to perform best; beyond 11 the discovery boost flattens and the post starts looking like spam to the algorithm.
Other fields: bio 150 chars, display name 30 chars, DM 1,000 chars.
### LinkedIn: 3,000 post, 1,300 sweet spot, "see more" fold
The **linkedin character limit** for posts is 3,000, but feed displays only the first 210 characters before the "see more" fold. Posts in the 1,200-1,500 character range win engagement on LinkedIn (multiple Buffer and Hootsuite studies converge on around 1,300 as the peak); they're long enough to demonstrate value, short enough not to wear out the scroll.
LinkedIn Articles (the long-form publishing surface) allow 110,000 characters, which is effectively unlimited. Profile headlines cap at 220, about-section text at 2,600.
### Facebook: 63,206 chars, 80-char organic sweet spot
Facebook's 63,206-character post limit is mostly trivia; in practice posts under 80 characters get about 30% higher organic engagement than longer ones (HubSpot consistently reports this across years). Above the fold, desktop shows about 477 characters; mobile cuts at around 125.
Comment max is 8,000 characters. Reactions, shares, and click-throughs all skew toward shorter posts, so long copy belongs in the linked article, not the Facebook caption.
### Newer platforms: Bluesky, Mastodon, Threads, TikTok
- **Bluesky** posts cap at 300 characters and are the unusual case: Bluesky counts grapheme clusters, so the seven-codepoint family emoji 👨👩👧👦 costs 1 character, not 7
- **Mastodon** defaults to 500 characters per toot, but instance admins can raise this to 5,000 or even unlimited; check the instance you're posting from
- **Threads** uses Twitter-style 500-character limits with codepoint counting
- **TikTok** captions allow 2,200 characters with about 100 shown above the fold
### Reddit, Discord, Slack: long-form and community defaults
- **Reddit** title 300 characters (subreddit moderators often enforce <60 via AutoModerator); comments 10,000 characters
- **Discord** standard message 2,000 characters; embed descriptions 4,096; Nitro raises to 4,000 on plain messages
- **Slack** message 40,000 characters; above 2,000 readability drops sharply and many recipients ignore long messages
## Word count targets by content type
Character limits dominate social and SEO; word counts dominate everything else: academic work, billing, content marketing, manuscripts. The table below gives a target range and a reading-time estimate (230 wpm, the Brysbaert 2019 silent-reading meta-analysis median) for each common content type.
| Content type | Word target | Reading time @ 230 wpm | Notes |
|---|---|---|---|
| Tweet | 30-40 words | 10 sec | optimize for character, not word |
| LinkedIn post (sweet spot) | 170-250 words | 1 min | above the fold |
| Instagram caption (hook) | 20-25 words | <10 sec | first 125 chars |
| Blog post — short | 500-700 words | 2-3 min | listicle, news, hot take |
| Blog post — standard | 1,000-1,500 words | 4-7 min | tutorial, deep guide |
| Blog post — long | 2,000-3,000 words | 9-13 min | comprehensive guide |
| SEO pillar page | 2,500-5,000 words | 11-22 min | topical authority |
| Academic essay (high school) | 500-1,500 words | 2-7 min | varies by assignment |
| Academic essay (undergrad) | 1,500-3,000 words | 7-13 min | per assignment |
| NaNoWriMo daily | 1,667 words/day | — | 50K words in 30 days |
| Novel — short | 50,000-70,000 words | — | YA, mystery |
| Novel — standard | 80,000-100,000 words | — | adult fiction |
| Conference talk (12 min @ 130 wpm) | 1,500-1,600 words | speaking | rehearse to confirm |
| Podcast episode (30 min @ 130 wpm) | 3,900 words | speaking | scripted portion |
Reading time is the more useful target unit for content marketing; readers respond to a "5-minute read" label more reliably than to a "1,150 words" label. Word count remains the unit for billing (translation invoiced per source word), platform compliance (NaNoWriMo's 50K, an academic 2,000-word ceiling), and contract terms. The [Word Counter](/tools/word-counter) shows both in real time as you type, plus speaking time at 130 wpm for talks and podcasts.
## 6 counting mistakes that break real apps
Six recurring failures seen in shipped code and shipped marketing campaigns. Each one is paired with the symptom, the root cause, and the fix.
### Mistake 1: Using `string.length` for character-limit validation
**Symptom:** A user pastes a tweet with three emoji that's actually 270 codepoints. Your front-end validation says 276 and refuses to submit. Or, worse, your code accepts a 285-codepoint draft because the emoji budget cancels out, and Twitter rejects it server-side.
**Root cause:** `String.prototype.length` in JavaScript returns UTF-16 code units. Every emoji is a surrogate pair, costing 2 units. Every astral-plane character (math symbols, ancient scripts) does the same.
**Fix:** Iterate by codepoint with the spread operator or `Array.from`.
```javascript
// ❌ wrong
function isUnderTwitterLimit(text) {
return text.length <= 280;
}
// ✅ correct
function isUnderTwitterLimit(text) {
return [...text].length <= 280;
}
```
For deeper regex-based codepoint iteration patterns (including grapheme cluster handling), the [Regex Cheat Sheet](/blog/regex-regular-expression-cheat-sheet-guide) covers the `/u` and `/v` flags and Unicode property escapes.
### Mistake 2: Splitting CJK text on whitespace for word count
**Symptom:** A 500-character Chinese article reports as 1 word. The translation quote based on it is off by 500x.
**Root cause:** CJK languages don't use word-spaces. `text.split(/\s+/)` returns a single token containing the entire essay.
**Fix:** Count each CJK ideograph as one word, which is the convention used by Microsoft Word, Google Docs, and every native CJK word processor.
```javascript
function countWordsMixed(text) {
const cjk = (text.match(/[一-鿿-ヿ가-]/g) || []).length;
const latin = (text
.replace(/[一-鿿-ヿ가-]/g, ' ')
.match(/[A-Za-z0-9]+(?:['’-][A-Za-z0-9]+)*/g) || []).length;
return cjk + latin;
}
```
The Unicode ranges cover CJK Unified Ideographs (U+4E00 to U+9FFF), Hiragana and Katakana (U+3040 to U+30FF), and Hangul Syllables (U+AC00 to U+D7AF), which are the four blocks Microsoft Word's word-count counts as ideographs.
### Mistake 3: Forgetting Twitter URL 23-char substitution
**Symptom:** A draft shows 320 characters in your counter, including an 80-character URL. You spend 10 minutes trimming it, only to realize Twitter would have accepted the original at 263 characters.
**Root cause:** Twitter replaces every URL with a 23-character `t.co` link at publish time. Your raw counter doesn't know.
**Fix:** Pre-compute published length using `raw − URL_length + 23` for each URL. For drafts containing multiple URLs, sum the corrections. URL detection in published content follows RFC 3986, the same parsing rules the [URL Encoding & Decoding](/blog/url-encoding-decoding-guide) guide walks through.
### Mistake 4: Writing meta description to 320 chars (old guideline)
**Symptom:** You crafted a 280-character meta description with the CTA at the end. In Google search results, the description cuts off mid-sentence at character 158 and the CTA never appears.
**Root cause:** Between December 2017 and May 2018, Google briefly expanded meta description display to 320 characters. Many SEO tutorials still cite that number. Google reverted to ~160 in mid-2018 and has held there ever since.
**Fix:** Write to 150-160 characters. Put the primary keyword in the first 30 characters and the CTA in the last 30. Use a pixel-accurate SERP simulator for high-stakes pages; wide glyphs (`W`, `M`, `K`) eat the budget faster than narrow ones (`i`, `l`, `t`).
### Mistake 5: Confusing 280 characters with 280 words
**Symptom:** Someone on the team writes "we need a 280-word tweet" and produces 1,500 characters of perfectly fine prose. The tweet won't post.
**Root cause:** Character-versus-word confusion. The two units differ by roughly 5-6x for English prose.
**Fix:** Pin the rule per platform. Twitter, SMS, and SEO meta count characters. NaNoWriMo, academic assignments, translation contracts, and most content-marketing briefs count words. When in doubt, check the platform's own counter (Twitter's compose box, Word's Review > Word Count) before locking the spec.
### Mistake 6: Pasting smart quotes that silently switch SMS to UCS-2
**Symptom:** You copy a customer-receipt template from a Google Doc into your SMS sender. The original was 145 characters and shipped as one GSM-7 segment. After paste, it's the same 145 characters but bills as 2 UCS-2 segments. Costs double across a million-message campaign.
**Root cause:** Google Docs and Word auto-convert `"` and `'` to typographer's quotes `" "` and `' '`. Those quotes aren't in the GSM-7 character set, which flips the entire message to UCS-2.
**Fix:** Normalize before transmit:
```javascript
function toGsm7Quotes(s) {
return s
.replace(/[“”]/g, '"') // " " → "
.replace(/[‘’]/g, "'") // ' ' → '
.replace(/[–—]/g, '-'); // – — → -
}
```
Run this before billing-sensitive sends. Twilio, MessageBird, and Bandwidth all expose an encoding field on the response; log it and alert when UCS-2 appears in templates you intended as GSM-7.
## FAQ
### What is the difference between character count and word count?
Character count counts every character including spaces, punctuation, and emoji, measured by Unicode codepoint on most modern platforms. Word count counts whitespace-separated tokens for Latin scripts and ideograph-by-ideograph for CJK. Twitter, SMS, and SEO meta descriptions use character count. Academic essays, NaNoWriMo manuscripts, and translation invoices use word count.
### Why does Twitter count emoji as 1 character but JavaScript counts them as 2?
Twitter measures by Unicode code point, and every emoji is one codepoint, one character. JavaScript's `string.length` measures UTF-16 code units. Most emoji are above U+FFFF and are encoded as surrogate pairs in UTF-16, so they take two code units and `.length` returns 2. Use `[...text].length` or `Array.from(text).length` to get the codepoint count Twitter actually counts.
### Why is the SMS character limit 160 sometimes and 70 other times?
SMS uses 7-bit GSM-7 encoding by default, giving 160 characters in a 140-byte payload. If the message contains any non-GSM-7 character (emoji, smart quotes, CJK, accented Latin beyond a small set), the whole message switches to 16-bit UCS-2 encoding and the per-segment limit drops to 70 characters. One emoji anywhere in the message triggers the switch.
### What is the ideal meta description length in 2026?
Aim for 150-160 characters. Google's desktop SERP truncates around 155-165 depending on display pixel width; mobile clips between 100 and 120. Below 120 characters Google often replaces your description entirely with a passage from page body. Lead with the primary keyword in the first 30 characters and end with the CTA in the last 30, so the message survives truncation either direction.
### Does character limit include spaces and emoji?
Yes, on virtually every platform. Spaces, line breaks, punctuation, and emoji each count as one Unicode codepoint. The two exceptions worth knowing: SMS where emoji trigger the encoding switch described above, and Bluesky which counts grapheme clusters so a multi-codepoint emoji like the family 👨👩👧👦 costs 1 character instead of 7.
### How is word count calculated for Chinese, Japanese, Korean text?
Each CJK ideograph counts as one word, the convention used by Microsoft Word's Chinese-mode word count, Google Docs, native CJK editors, and every commercial translation memory system. A 500-character Chinese essay reports as 500 words. Mixed text counts CJK ideographs by character and Latin tokens by whitespace, summing the two.
### How does Twitter handle URL length in the 280-character limit?
Twitter automatically wraps every URL in a 23-character `t.co` short link at publish time, regardless of original length. The published length follows the formula `published = raw − URL_length + 23` per URL. A draft of 320 characters containing one 100-character URL ships as 243 characters. Twitter recognizes URLs by RFC 3986 patterns, so query strings and fragments are absorbed into the URL token.
## Related reading
- [Regex Cheat Sheet](/blog/regex-regular-expression-cheat-sheet-guide): pattern matching for character validation, Unicode property escapes
- [Text Diff Online Guide](/blog/text-diff-online-compare-tool-guide): comparing two pieces of text, line by line and character by character
- [URL Encoding & Decoding Guide](/blog/url-encoding-decoding-guide): character escaping rules when text travels through URLs
- [Understanding Base64](/blog/understanding-base64): the other half of "bits into characters" encoding, applied to email and binary data
---
### CIDR Notation Explained: Subnet Masks from /8 to /32
URL: https://go-tools.org/blog/cidr-notation-subnet-mask-cheat-sheet-guide
A /26 leaves 62 usable hosts, not 64, and on a /31 the minus-two rule breaks. Read any CIDR prefix, do the mask in your head, check it in the calculator.
# CIDR Notation and Subnet Masks: How to Read /8 Through /32
The number after the slash in `192.168.1.0/24` counts bits, not addresses. It says how many of the 32 bits in an IPv4 address belong to the network; whatever is left over belongs to the hosts. That one sentence is most of what CIDR notation is.
A /24 leaves 8 host bits, so the block holds 2⁸ = 256 addresses. A /26 leaves 6, so it holds 64. Every bit you hand back to the host side doubles the block; every bit you take doubles the number of blocks. The count you can actually assign to devices is normally two lower than the total, because the first address names the network and the last one is the broadcast. A /26 gives you 62 usable hosts, not 64.
Two prefixes break that minus-two rule on purpose, and a wildcard mask is not a subnet mask even though the two get pasted into each other's fields all the time. If you only want the answer for one block, the [subnet calculator](/tools/subnet-calculator) prints it; this article is about getting there without one.
## What CIDR notation says
A subnet mask is itself a 32-bit number, written the way an address is written. Its bits are a run of 1s followed by a run of 0s. The 1s mark network bits, the 0s mark host bits. Spell a /26 out in full and you get:
```
11111111.11111111.11111111.11000000
255 . 255 . 255 . 192
```
Convert each octet back to decimal and that is 255.255.255.192. CIDR notation counts the leading 1s instead of writing all thirty-two of them out. `192.168.1.0/26` and `192.168.1.0 255.255.255.192` are the same statement in two syntaxes, and which one a device wants depends entirely on the command you are typing.
/8, /16 and /24 land on octet boundaries, so they look tidy in decimal: 255.0.0.0, 255.255.0.0, 255.255.255.0. Nothing about the notation requires that. /22 and /27 are just as valid. They cut through the middle of an octet and produce masks like 255.255.252.0 that look arbitrary right up until you write them in binary.
Before 1993 the leading bits of an address decided its size: class A took a /8, class B a /16, class C a /24, and there was nothing in between. An organisation with 300 hosts had to claim a class B and waste more than 65,000 addresses, or take two class C blocks and carry two routes. CIDR (RFC 1519, later revised as RFC 4632) broke that coupling. The prefix travels with the address, so a block can be any power of two. Classes still turn up in certification exams and in old documentation, but classful routing has been obsolete since CIDR arrived in 1993. The mask decides where a network ends, not the first octet.
## The subnet mask cheat sheet, /8 to /32
The last column of this CIDR to subnet mask chart is deliberate: it shows what the generic `2ⁿ − 2` formula produces, which matches the real usable count everywhere except the bottom two rows.
| Prefix | Subnet mask | Wildcard mask | Total addresses | Usable hosts | Naive `2ⁿ − 2` gives |
|---|---|---|---|---|---|
| /8 | 255.0.0.0 | 0.255.255.255 | 16777216 | 16777214 | 16777214 |
| /9 | 255.128.0.0 | 0.127.255.255 | 8388608 | 8388606 | 8388606 |
| /10 | 255.192.0.0 | 0.63.255.255 | 4194304 | 4194302 | 4194302 |
| /11 | 255.224.0.0 | 0.31.255.255 | 2097152 | 2097150 | 2097150 |
| /12 | 255.240.0.0 | 0.15.255.255 | 1048576 | 1048574 | 1048574 |
| /13 | 255.248.0.0 | 0.7.255.255 | 524288 | 524286 | 524286 |
| /14 | 255.252.0.0 | 0.3.255.255 | 262144 | 262142 | 262142 |
| /15 | 255.254.0.0 | 0.1.255.255 | 131072 | 131070 | 131070 |
| /16 | 255.255.0.0 | 0.0.255.255 | 65536 | 65534 | 65534 |
| /17 | 255.255.128.0 | 0.0.127.255 | 32768 | 32766 | 32766 |
| /18 | 255.255.192.0 | 0.0.63.255 | 16384 | 16382 | 16382 |
| /19 | 255.255.224.0 | 0.0.31.255 | 8192 | 8190 | 8190 |
| /20 | 255.255.240.0 | 0.0.15.255 | 4096 | 4094 | 4094 |
| /21 | 255.255.248.0 | 0.0.7.255 | 2048 | 2046 | 2046 |
| /22 | 255.255.252.0 | 0.0.3.255 | 1024 | 1022 | 1022 |
| /23 | 255.255.254.0 | 0.0.1.255 | 512 | 510 | 510 |
| /24 | 255.255.255.0 | 0.0.0.255 | 256 | 254 | 254 |
| /25 | 255.255.255.128 | 0.0.0.127 | 128 | 126 | 126 |
| /26 | 255.255.255.192 | 0.0.0.63 | 64 | 62 | 62 |
| /27 | 255.255.255.224 | 0.0.0.31 | 32 | 30 | 30 |
| /28 | 255.255.255.240 | 0.0.0.15 | 16 | 14 | 14 |
| /29 | 255.255.255.248 | 0.0.0.7 | 8 | 6 | 6 |
| /30 | 255.255.255.252 | 0.0.0.3 | 4 | 2 | 2 |
| /31 | 255.255.255.254 | 0.0.0.1 | 2 | **2** | **0** |
| /32 | 255.255.255.255 | 0.0.0.0 | 1 | **1** | **−1** |
The table is more useful as a set of relationships than as a list of answers. Each row down the table halves the block: /24 holds 256 addresses, /25 holds 128, /26 holds 64. The wildcard column is the subnet mask with every bit flipped, which is why 255.255.255.192 and 0.0.0.63 always appear on the same line. The two bold rows are where the standard formula stops describing reality.
Only the last octet values in the mask column need memorising, because they repeat: 128, 192, 224, 240, 248, 252, 254, 255. Those are the only eight non-trivial byte values a valid mask can end with. If you want to see why, the [number base converter](/tools/base-converter) prints any of them in binary.
### Reading the table backwards: from subnet mask to CIDR
Given a dotted-decimal mask, count the 1 bits. Every 255 contributes 8, and the one interesting octet contributes the rest:
| Last mask octet | 128 | 192 | 224 | 240 | 248 | 252 | 254 | 255 |
|---|---|---|---|---|---|---|---|---|
| Bits it adds | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
So 255.255.255.192 is 8 + 8 + 8 + 2 = /26. And 255.255.252.0 is 8 + 8 + 6 + 0 = /22, because 252 in binary is `11111100`.
Two consequences follow. Any octet that is neither 255 nor 0 is the boundary octet, and a valid mask can only ever have one of them. The value of that octet also gives you the block size directly.
## How to calculate a subnet mask and network by hand
The mechanical method has three steps and works for any prefix. The worked example below uses `192.168.1.130/26`, but the point is the procedure, not this address.
### Step 1: find the block size
Block size is `256 − the boundary mask octet`.
For a /26 the mask is 255.255.255.192, so the block size is `256 − 192 = 64`. Subnets of that size sit on multiples of 64 in the fourth octet: 0, 64, 128, 192. There are no other possible starting points.
Block size and address count are the same number, reached from the mask side instead of the bit-count side.
### Step 2: find which block the address falls in
Divide the boundary octet of the address by the block size and round down.
The address is 192.168.1.**130**, the block size is 64, so `130 ÷ 64 = 2.03…`, which rounds down to `2`. Multiply back: `2 × 64 = 128`. The address sits in the block that starts at 128.
Most errors happen here, always in the same direction: people assume the address they were handed is the start of its block, and it usually is not. `192.168.1.130` is a host address living in the third /26 of that /24.
### Step 3: network, broadcast, first and last host
The block start is the network address. The broadcast is the block start plus the block size minus one. Everything strictly between them is assignable:
```
Network 192.168.1.128
Broadcast 192.168.1.191
Usable 192.168.1.129 - 192.168.1.190
Usable 62 (of 64 total)
Netmask 255.255.255.192
Wildcard 0.0.0.63
```
Broadcast is `128 + 64 − 1 = 191`. First host is network + 1, last host is broadcast − 1, and the usable count is 64 − 2 = 62, which is what the /26 row of the cheat sheet says.
Compressed into something you can say to yourself: **block size is 256 minus the mask octet; round the address down to a multiple of it; that is your network, and the next block start minus one is your broadcast.**
The method is not specific to the fourth octet. For a /22 the mask is 255.255.252.0, so the boundary octet is the third and the block size there is `256 − 252 = 4`. Blocks therefore start at 10.0.0.0, 10.0.4.0, 10.0.8.0, and the block `10.0.0.0/22` runs through broadcast 10.0.3.255 with usable addresses 10.0.0.1 to 10.0.3.254 — four consecutive /24s inside one broadcast domain. Same three steps, different octet.
If the binary-to-decimal side of this is where you slow down, the [guide to number base conversion](/blog/number-base-conversion-binary-hex-octal-guide) covers the conversion itself in more depth than a subnetting article should.
For checking your work in a script rather than in your head, Python's standard library already knows all of this:
```python
import ipaddress
net = ipaddress.ip_network("192.168.1.130/26", strict=False)
print(net) # 192.168.1.128/26
print(net.network_address) # 192.168.1.128
print(net.broadcast_address) # 192.168.1.191
print(net.netmask) # 255.255.255.192
print(net.hostmask) # 0.0.0.63
print(net.num_addresses) # 64
print(len(list(net.hosts()))) # 62
```
`strict=False` is what lets you pass a host address instead of a network address; with the default `strict=True` the same call raises `ValueError`.
## Where the minus-two rule stops being true
The subtraction has a reason behind it. In an ordinary subnet the all-zeros host pattern names the network itself and the all-ones host pattern is the directed broadcast address. Neither can be configured on an interface, so a block of 2ⁿ addresses offers 2ⁿ − 2 to hosts. That is why a /24 gives 254 and a /26 gives 62.
The reason is also the limit: when a block is too small to contain those two reserved addresses, subtracting them stops making sense.
**A /31 has two addresses and two usable hosts.** RFC 3021 defines /31 for point-to-point links. Such a link has exactly two endpoints and no shared segment, so there is nothing for a broadcast address to do and nothing for a network address to identify. Both addresses go to the two ends. Applying `2ⁿ − 2` here returns 0, because the formula assumes a topology this link does not have. The condition attached is real: /31 is valid only on genuinely point-to-point interfaces. A multi-access LAN segment still needs /30 or shorter, and Windows will not accept a /31 on a NIC.
**A /32 has one address and one usable host.** It is a single host route: loopback interfaces, static routes, anycast addresses, single-address firewall rules. There is no broadcast address, so the formula's `− 2` would return −1.
Python agrees with both:
```python
import ipaddress
p2p = ipaddress.ip_network("203.0.113.4/31")
print([str(h) for h in p2p.hosts()]) # ['203.0.113.4', '203.0.113.5']
host = ipaddress.ip_network("10.0.0.1/32")
print([str(h) for h in host.hosts()]) # ['10.0.0.1']
```
It is easier to remember this as one rule than as two exceptions: the subtraction removes two specific addresses, so check that they exist before you remove them. A /31 and a /32 have no broadcast address at all, so nothing is removed.
### Total, usable, and number of subnets are three different numbers
These three get conflated constantly, and the confusion is understandable because they are all powers of two derived from the same prefix.
- **Total addresses** in a /p is `2^(32 − p)`. A /26: 64.
- **Usable hosts** is that minus 2, except for /31 and /32. A /26: 62.
- **Number of subnets** you get by splitting a parent /p into children /q is `2^(q − p)`. Splitting a /24 into /26s borrows two bits, so `2² = 4` subnets.
The three answer different questions, so a sentence like "a /26 gives you 4" is only true if the question was about splitting a /24. Splitting also costs you addresses, because every child subnet reserves its own network and broadcast pair. Four /26s carved out of a /24 hold `4 × 62 = 248` usable addresses against the parent's 254. Six addresses go to the split itself.
## Wildcard mask vs subnet mask: which command wants which
A wildcard mask is the bitwise inverse of the subnet mask. Where the subnet mask has 1s, the wildcard has 0s. Take the /26 row of the cheat sheet: mask 255.255.255.192, wildcard 0.0.0.63. Flip every bit of one and you have the other, so they never appear apart.
The two are used in opposite senses, and that is where people trip. A subnet mask is applied with a bitwise AND, so a 1 bit means "this bit is part of the network". A wildcard is a match filter, so a 0 bit means "this bit must match" and a 1 bit means "don't care". Same underlying operation, opposite polarity. If the bit-level mechanics are fuzzy, the [guide to bitwise operations](/blog/bitwise-operations-complete-guide) covers AND, OR and NOT in more general terms.
Which one a device wants is not a matter of preference. For the block 192.168.1.128/26:
```
! wants the subnet mask
ip address 192.168.1.129 255.255.255.192
! wants the wildcard mask
access-list 10 permit 192.168.1.128 0.0.0.63
network 192.168.1.128 0.0.0.63 area 0
```
```
! Cisco ASA — wants the subnet mask, unlike IOS ACLs
access-list OUT permit ip 192.168.1.128 255.255.255.192 any
```
```bash
# Linux iproute2 — takes the prefix directly
ip addr add 192.168.1.129/26 dev eth0
```
Cisco IOS ACLs and OSPF network statements take the wildcard mask; the ASA takes the subnet mask instead. That is one vendor with two conventions in the same product family, and it is the most common copy-paste failure in the area. Other platforms have their own conventions, so before pasting a value into an unfamiliar field, confirm which of the two that field expects.
Paste 255.255.255.192 into an IOS ACL and the router reads it as a wildcard: three octets of all-1s mean "don't care", so the first three octets stop being matched at all and the rule reaches far outside the block you had in mind. The router reports no syntax error and logs nothing; you are left with a permit statement that has the wrong scope. The reverse mistake at least has a chance of being caught, because 0.0.0.63 is not a valid subnet mask; it has no leading run of 1s. Whether a given platform rejects it or accepts it quietly is not something to discover in production.
### Why ACL wildcards may have gaps but subnet masks may not
A subnet mask must be one contiguous run of 1s followed by 0s. That requirement is what makes the AND operation split an address into exactly two parts. A value like 255.0.255.0 has a hole in it, describes no coherent boundary, and devices reject it. So does Python:
```python
import ipaddress
ipaddress.ip_network("10.0.0.0/255.0.255.0")
# ValueError: '10.0.0.0/255.0.255.0' does not appear to be an IPv4 or IPv6 network
```
Only 33 masks are valid, /0 through /32. Anything else is a typo.
ACL wildcards are under no such constraint, because they are not splitting an address into a network and a host part. They are a per-bit match filter, so gaps are legal and occasionally useful. A single wildcard with a gap can match every odd-numbered address in a range, for instance. That difference is why the two values cannot be swapped: they are not the same kind of object, they just happen to look alike in dotted-decimal.
## Private, CGNAT and other reserved ranges
A prefix tells you how big a block is. Which block it is tells you whether it is yours to use.
| Block | Range | Reserved by |
|---|---|---|
| 10.0.0.0/8 | 10.0.0.0 – 10.255.255.255 | RFC 1918 private |
| 172.16.0.0/12 | 172.16.0.0 – 172.31.255.255 | RFC 1918 private |
| 192.168.0.0/16 | 192.168.0.0 – 192.168.255.255 | RFC 1918 private |
| 100.64.0.0/10 | 100.64.0.0 – 100.127.255.255 | RFC 6598 carrier-grade NAT |
| 169.254.0.0/16 | 169.254.0.0 – 169.254.255.255 | RFC 3927 link-local |
| 255.255.255.255/32 | single address | limited broadcast |
The RFC 1918 ranges are never routed on the public internet, which is what makes them safe to allocate from. The other three show up for different reasons.
`100.64.0.0/10` is carrier-grade NAT space. If your ISP hands you an address in it, you are behind their NAT and no inbound connection will reach you without a tunnel. It is not private space in the RFC 1918 sense and it is not yours to use in an internal plan, because your provider may already be using it on the other side of your router.
`169.254.0.0/16` is link-local. A host assigns itself one of these when DHCP fails, so seeing a 169.254 address on an interface is a diagnosis rather than a configuration: nothing answered the DHCP request. Traffic to it never crosses a router.
For examples in documentation and runbooks, RFC 5737 reserves 192.0.2.0/24, 198.51.100.0/24 and 203.0.113.0/24 precisely so that a copy-pasted example cannot point at a real host.
### 172.16.0.0/12 is sixteen /16s, not one
People get this reserved range wrong because of the prefix. A /12 borrows four bits from the second octet, so the block spans 172.16.0.0 through 172.31.255.255: sixteen consecutive /16s, not just `172.16.x.x`.
The consequences run both ways. An address like 172.20.5.1 **is** private, sitting comfortably inside the range, even though it does not start with 172.16. And 172.15.x.x and 172.32.x.x are **public** addresses belonging to somebody else, so a firewall rule or a "trust the internal range" check written against 172.0.0.0/8 quietly trusts a large slice of the internet.
If you need to confirm a boundary like this, the cheat sheet gives you the arithmetic: a /12 has `2^(32−12)` addresses, the second octet moves in steps of `256 − 240 = 16`, and 16 + 16 = 32, so the block ends immediately before 172.32.0.0.
## VLSM: splitting one block into unequal subnets
Equal-sized subnets are easy and usually wrong. A branch office given a single 192.168.1.0/24 might need four segments with nothing in common: a hundred workstations, fifty phones, a dozen servers, and a handful of management interfaces. Split the /24 into four equal /26s and the workstation segment overflows at 62 hosts, while the management segment sits on 62 addresses to serve ten devices.
Variable Length Subnet Masking means giving each segment the prefix it actually needs. The table below carves the same /24 into one /25, one /26 and two /28s.
**Allocate from largest to smallest.** Each block must start on a multiple of its own size, so the biggest block gets first pick:
| Segment | Hosts needed | Prefix | Network | Usable range | Broadcast | Subnet mask |
|---|---|---|---|---|---|---|
| Workstations | 100 | /25 | 192.168.1.0 | 192.168.1.1 – 192.168.1.126 | 192.168.1.127 | 255.255.255.128 |
| Voice | 50 | /26 | 192.168.1.128 | 192.168.1.129 – 192.168.1.190 | 192.168.1.191 | 255.255.255.192 |
| Servers | 12 | /28 | 192.168.1.192 | 192.168.1.193 – 192.168.1.206 | 192.168.1.207 | 255.255.255.240 |
| Management | 10 | /28 | 192.168.1.208 | 192.168.1.209 – 192.168.1.222 | 192.168.1.223 | 255.255.255.240 |
| *unallocated* | — | /27 | 192.168.1.224 | 192.168.1.225 – 192.168.1.254 | 192.168.1.255 | 255.255.255.224 |
Walking it with the three-step method:
1. The /25 has block size `256 − 128 = 128`, so it starts at 0 and its broadcast is `0 + 128 − 1 = 127`. Usable 1 to 126, which is 126 addresses, enough for 100 workstations with room left. The cheat sheet's /25 row agrees: 128 total, 126 usable.
2. The next free address is 128. The /26 has block size 64, and 128 is a multiple of 64, so it fits: network 192.168.1.128, broadcast `128 + 64 − 1 = 191`, usable 129 to 190. That is 62 usable for 50 phones. Cheat sheet /26 row: 64 total, 62 usable.
3. Next free is 192. The /28 has block size 16, and `192 = 16 × 12`, so it fits: network 192.168.1.192, broadcast 207, usable 193 to 206, which is 14 addresses for 12 servers. Cheat sheet /28 row: 16 total, 14 usable.
4. Next free is 208, and `208 = 16 × 13`, so the second /28 lands at 192.168.1.208 with broadcast 223 and usable 209 to 222.
That accounts for `128 + 64 + 16 + 16 = 224` of the 256 addresses, leaving 192.168.1.224 through 192.168.1.255. Those 32 addresses happen to be exactly one aligned /27, which is where the point-to-point links would come from later: sixteen /31s fit inside it, one per router link.
**Why largest first, and what it costs when you do not.** Suppose you place the two /28s at the bottom instead: 192.168.1.0/28 and 192.168.1.16/28. The next free address is 192.168.1.32, and a /25 must start on a multiple of 128, so it cannot start there. It has to skip forward to 192.168.1.128. The addresses from 32 to 127 are not lost, but they are only usable as smaller aligned pieces (a /27 at 192.168.1.32 and a /26 at 192.168.1.64), so the leftover space ends up scattered instead of sitting in one contiguous block at the top. Add one more segment to the request and the same manoeuvre stops fitting at all.
Ordering by size avoids that. After you place a block, the next free address is a multiple of that block's size, and a multiple of a larger power of two is automatically a multiple of every smaller one, so every subsequent, smaller block is aligned wherever the previous one ended. You never have to skip. Before committing a plan like this to a switch, running it through the [subnet calculator](/tools/subnet-calculator) division table is faster than checking the alignment of every segment by hand.
## Five mistakes that survive into production
**1. Treating the address you typed as the network address**
**Symptom:** a firewall rule matches nothing, or a route covers the wrong half of a segment.
**Cause:** `192.168.1.130/26` was read as network 192.168.1.0, broadcast 192.168.1.255: the /24 boundary, because that is the one people see in decimal.
**Fix:** apply step 2. Block size 64, `130 ÷ 64` rounds down to 2, so the network is `2 × 64 = 128`. The block is 192.168.1.128 to 192.168.1.191, and 192.168.1.0 is a different subnet entirely. Any time a prefix is longer than /24, assume the address you were given is a host address until you have masked it.
**2. Applying `2ⁿ − 2` to a /31**
**Symptom:** an IPAM tool or a spreadsheet reports 0 usable hosts for a point-to-point link that is up and passing traffic.
**Cause:** the minus-two rule assumes a network and a broadcast address exist to be subtracted. On a /31 they do not.
**Fix:** treat /31 and /32 as the boundary conditions of the formula rather than as anomalies. RFC 3021 makes both addresses of a /31 assignable, and a /32 is a single host route with one address. Anything that reports 0 or −1 has applied the formula outside its domain. Check /31 support on the specific interface first, since the exception only holds on genuinely point-to-point links.
**3. Treating 172.16.0.0/12 as `172.16.x.x` only**
**Symptom:** an internal service is unreachable from one office, or a "block all private ranges" rule leaks.
**Cause:** the /12 was read as if it were a /16.
**Fix:** the range is 172.16.0.0 to 172.31.255.255. Write ACLs and allowlists against the prefix 172.16.0.0/12 rather than against an octet pattern, and remember that 172.15.x.x and 172.32.x.x sit outside it on the public internet. If you are matching by hand, the second octet is the boundary octet and it steps by 16.
**4. Pasting a wildcard into a field that wants a subnet mask**
**Symptom:** an ACL permits far more, or far less, than intended, and nothing in the config looks wrong.
**Cause:** IOS ACLs and OSPF network statements take the wildcard while the ASA takes the subnet mask, and 0.0.0.63 and 255.255.255.192 are visually interchangeable at a glance.
**Fix:** check the field before pasting, not after. A useful tell: for any prefix of /8 or longer, a subnet mask begins with 255 and a wildcard begins with 0. If a value sitting in an ACL or an OSPF network statement begins with 255, it is a subnet mask in a wildcard field.
**5. Writing a non-contiguous mask, or generating one by accident**
**Symptom:** a device rejects a configuration line, or a home-grown script produces masks that look plausible and are wrong.
**Cause:** a value like 255.0.255.0 is not a valid subnet mask — it has a hole. The scripted version is subtler: in JavaScript, shift operators take their right operand modulo 32, so an out-of-range prefix silently produces a plausible-looking wrong mask (33 becomes /1, −1 becomes /31) instead of throwing.
**Fix:** validate the prefix range before shifting, and reject any mask that is not a solid run of 1s followed by 0s. A library that raises on bad input is worth more here than one that guesses, because both failure modes are silent.
## FAQ
### What is the difference between a subnet and a VLAN?
A VLAN is a layer 2 broadcast domain configured on switches; a subnet is a layer 3 range of addresses. They are usually mapped one to one, but nothing enforces that: you can put two subnets on one VLAN, or trunk one VLAN across sites. Renumber the subnet and the VLAN ID does not change.
### How many subnets do I get if I split a /24 into /26s?
Four. The count is 2 raised to the number of borrowed bits, and /26 is two bits longer than /24, so `2² = 4` subnets of 64 addresses each. Each child reserves its own network and broadcast address, so the four /26s hold 248 usable addresses against the parent /24's 254.
### Does CIDR notation work the same way in IPv6?
The slash still counts leading network bits, so /64 means 64 network bits out of 128. What does not carry over is the minus-two rule: IPv6 has no broadcast address, so nothing is subtracted from the total. The cheat sheet above and the calculator behind it are IPv4 only.
### What does 0.0.0.0/0 mean?
Zero network bits, so it matches every IPv4 address. In a routing table it is the default route, used when no more specific prefix matches. As a bind address it means "all interfaces", which is why a service listening on 0.0.0.0 is reachable from every network the machine is attached to.
### What happens if two subnets on the same network overlap?
Routers pick the more specific route, since forwarding always prefers the longest matching prefix, while hosts inside the overlap disagree about which destinations are local. The symptom is partial: some destinations work and some do not, and which ones changes depending on where you test from.
### Why were Class A, B and C addresses replaced by CIDR?
Because the classes only offered three sizes: /8, /16 and /24. An organisation needing 300 addresses had to take a class B and waste most of it or run two class C routes. CIDR let a prefix be any length, which slowed address exhaustion and let providers summarise many customer blocks into one route.
### Can I subnet a private range like 192.168.0.0/16 however I want?
Yes. RFC 1918 space is yours to divide at any prefix length and nobody outside your network sees it. The constraint is internal: overlapping with a partner network or a cloud VPC you later peer with is expensive to unwind, which is why plans tend to avoid the blocks every home router already uses.
**What to carry away.** The prefix counts network bits, and block size is `256 − the boundary mask octet`. Round the address down to a multiple of the block size for the network, and add block size minus one for the broadcast. Subtract two for the reserved pair, but only when a /31 or /32 has not already removed the reason to subtract. Keep the wildcard and the subnet mask straight by their shape. Allocate large blocks before small ones.
None of it needs a tool once the arithmetic is in your head, which is the reason to work through it by hand at least once. For checking a plan before it reaches a router, or for reading a block's binary boundary at a glance, the [subnet calculator](/tools/subnet-calculator) runs locally in your browser.
---
### cm to inches: exact formula, height & screen charts (2026)
URL: https://go-tools.org/blog/cm-to-inches-length-conversion-guide
Convert cm to inches (centimeters to inches) with the exact 1 in = 2.54 cm factor. Mental-math, height, screen, paper charts plus code. Free in browser.
# cm to inches conversion guide: exact formula, height & screen charts (2026)
`1 inch = 2.54 cm`. That number is exact, not a rounded approximation. The 1959 International Yard and Pound Agreement pinned the inch to that value, and every legal-for-trade ruler since then traces back to it. To convert cm to inches, divide by 2.54. To go the other way, multiply by 2.54.
```text
inches = cm ÷ 2.54
cm = inches × 2.54
```
> Need a number right now? Open the free [length converter](/tools/length-converter). 16 length units, instant results, runs entirely in your browser with full IEEE 754 precision.
This guide answers four questions in order. Where the 2.54 factor actually comes from, four mental-math tricks matched to four precision tiers, scenario charts (height, screens, paper, mm), and how centimeters to inches connects to the rest of the metric and imperial length family. JavaScript and Python snippets with a roundtrip assert close it out.
---
## The exact formula and where 2.54 comes from
The number `2.54` is not measured; it is defined. On 1 July 1959, the United States, United Kingdom, Canada, Australia, New Zealand and South Africa signed the International Yard and Pound Agreement, fixing the international yard at exactly `0.9144 m`. From that single definition every smaller imperial length falls out: 1 yard = 36 inches, so `1 in = 0.0254 m = 25.4 mm = 2.54 cm` exactly. NIST publishes the same factor in Handbook 44, which is what every certified caliper in a US machine shop is calibrated to.
The metric side hardened on 20 May 2019. The BIPM redefined the meter by fixing the speed of light at exactly `c = 299,792,458 m/s`, so 1 meter is now the distance light travels in `1/299,792,458` of a second in vacuum. The inch inherits that definition through the yard and meter chain. Practical effect for daily work: zero. What changed is that any lab with an iodine-stabilized laser can realize a meter from first principles, no platinum bar required.
Going the other direction, `1 cm = 0.3937007874… in`. That is an infinite non-repeating decimal in any base ten representation, which means the cm to in formula is asymmetric. cm-to-inch is divisive (clean), inch-to-cm is multiplicative (also clean), but neither direction has a "nicer" inverse. A worked example: `30 cm ÷ 2.54 = 11.811024 in`. Round to 11.81 in for shopping, keep all six digits for engineering drawings.
Precision warning. Replacing `0.3937007874` with `0.39` for speed introduces 0.6 mm of error per meter. Fine for picking out a curtain rod, fatal for a CNC tool path where ±0.05 mm is normal tolerance. When the work matters, use the full factor or the [length converter](/tools/length-converter), which carries the canonical `2.54` through to the result.
---
## 4 mental-math tricks that match your precision
The eight-digit factor is precise but useless in a furniture aisle or a shoe shop. These four tricks cover the realistic precision tiers, so pick the one that matches what's at stake.
### Method 1: halve, then subtract 20% (~1.6% error)
Divide the cm by 2, then knock off 20% of that halved number.
- 30 cm → `15 - 3 = 12 in` (exact 11.81 in).
- 50 cm → `25 - 5 = 20 in` (exact 19.69 in).
- 100 cm → `50 - 10 = 40 in` (exact 39.37 in).
The math: `0.5 - 0.1 = 0.4`, which is 1.6% high relative to the true `0.3937`. Use this for clothing sizes, bag dimensions, "will this fit on the shelf" questions.
### Method 2: multiply by 4, divide by 10 (~1.6% error, cleanest integers)
If you'd rather not handle fractional cm, multiply by 4 and shift the decimal once.
- 27 cm → `108 ÷ 10 = 10.8 in` (exact 10.63 in).
- 55 cm → `220 ÷ 10 = 22 in` (exact 21.65 in).
- 75 cm → `300 ÷ 10 = 30 in` (exact 29.53 in).
Same 1.6% bias as Method 1 but easier when the cm value is awkward to halve. Useful for screen sizing in a store where you just need to know whether a 27" monitor will look bigger than the 24" you have at home.
### Method 3: divide by 2.54 (full precision)
When 1.6% error is intolerable, just do the division. CNC paths, medical-device tolerances, customs declarations on cross-border parcels, and engineering drawings all need the full factor. A pocket calculator handles `÷ 2.54` in two keystrokes; a spreadsheet handles a thousand rows in milliseconds. This is also the cm to in formula that any conversion API will use under the hood.
### Method 4: roundtrip sanity check
Whichever shortcut you took, run the result back through `× 2.54` and confirm it lands close to where you started. Converted 75 cm to "about 30 in"? Multiply: `30 × 2.54 = 76.2 cm`. Within 1.6%, so the shortcut held. If the roundtrip differs by more than 5%, you dropped a factor of ten, usually a slipped decimal between mm and cm. Pilots use the same defensive trick converting fuel uplift between liters and gallons.
---
## Quick reference charts: cm and inches
Bookmark this section when you need a number without thinking. All values use the exact 2.54 factor and round to two or three decimals depending on use.
### Small scale (0.1 to 10 cm)
| cm | mm | inches | Reference |
| ---- | --- | ------- | ---------------------------- |
| 0.1 | 1 | 0.0394 | 1 mm = 0.0394 in |
| 0.5 | 5 | 0.1969 | 5 mm board thickness |
| 1 | 10 | 0.3937 | width of a fingernail |
| 2 | 20 | 0.7874 | thumb width |
| 2.54 | 25.4| 1.0000 | 1 inch (the anchor) |
| 3 | 30 | 1.1811 | A4 short edge / 30 cm rounds |
| 5 | 50 | 1.9685 | typical lipstick length |
| 10 | 100 | 3.9370 | 100 mm = 3.937 in |
### Height chart: cm to ft + in
The conversion every traveler needs eventually. Formulas:
```text
total_inches = cm ÷ 2.54
feet = floor(total_inches ÷ 12)
inches = total_inches − feet × 12
```
| cm | ft + in | Notes |
| ------ | ------- | ----------------------------------------- |
| 152.4 | 5'0" | exact |
| 157.5 | 5'2" | |
| 160.0 | 5'3" | global female-average reference |
| 162.6 | 5'4" | |
| 165.1 | 5'5" | exact |
| 167.6 | 5'6" | |
| 170.18 | 5'7" | exact (67 in × 2.54) |
| 172.7 | 5'8" | global male-average reference |
| 175.3 | 5'9" | |
| 177.8 | 5'10" | exact |
| 180.3 | 5'11" | |
| 182.88 | 6'0" | exact (72 in × 2.54) |
| 185.4 | 6'1" | |
| 187.96 | 6'2" | exact (74 in × 2.54) |
| 190.5 | 6'3" | exact |
| 193.04 | 6'4" | exact (76 in × 2.54) |
| 200.0 | 6'6.7" | basketball-roster threshold |
For height conversion the trick is to do all arithmetic in one go. Convert cm to total inches first, then split into feet and inches at the end. Splitting first and rounding twice causes rounding drift (covered in the mistakes section below).
### Screen size chart: diagonals 11" to 85"
TV and monitor size always means the diagonal. The visible width and height of a 16:9 panel are smaller than the diagonal, much smaller than buyers expect.
| diagonal | cm | 16:9 width | 16:9 height | Typical use |
| -------- | ------ | ---------- | ----------- | ---------------------- |
| 11" | 27.94 | 24.36 cm | 13.70 cm | netbook / iPad mini |
| 13.3" | 33.78 | 29.45 cm | 16.57 cm | 13" laptop |
| 15.6" | 39.62 | 34.55 cm | 19.43 cm | 15" laptop |
| 24" | 60.96 | 53.15 cm | 29.90 cm | budget desktop monitor |
| 27" | 68.58 | 59.78 cm | 33.62 cm | popular desktop tier |
| 32" | 81.28 | 70.85 cm | 39.85 cm | small TV / large monitor|
| 43" | 109.22 | 95.21 cm | 53.55 cm | mid-tier TV |
| 55" | 139.70 | 121.76 cm | 68.49 cm | living-room TV |
| 65" | 165.10 | 143.94 cm | 80.96 cm | large living-room TV |
| 85" | 215.90 | 188.21 cm | 105.87 cm | flagship TV |
The width formula is `diagonal × cos(arctan(9/16)) ≈ diagonal × 0.8716`; the height is `diagonal × 0.4903`. Measure the wall before buying. A 65" TV needs about 144 cm of clear horizontal space plus stand or mount clearance.
### Paper & document chart (A4 vs US Letter)
Cross-border printing trips up almost every remote team. The two standard sizes are close but never identical:
| Format | mm × mm | cm × cm | inches × inches |
| --------- | ---------- | ----------- | --------------- |
| A4 | 210 × 297 | 21.0 × 29.7 | 8.27 × 11.69 |
| US Letter | 215.9 × 279.4 | 21.59 × 27.94 | 8.5 × 11.0 |
| A3 | 297 × 420 | 29.7 × 42.0 | 11.69 × 16.54 |
| Legal | 215.9 × 355.6 | 21.59 × 35.56 | 8.5 × 14.0 |
A4 is 0.59 cm narrower but 1.76 cm taller than Letter. Print a Letter PDF on A4 paper without "fit to page" and the bottom line of every page can clip; print A4 on Letter and the right margin shrinks. For a wider tour of the metric and imperial families, see our [unit conversion complete guide](/blog/unit-conversion-complete-guide).
---
## 5 real-world scenarios when cm and inches matter
### Height on a medical form: 5'7" to cm
US clinical intake forms still ask for feet and inches; the WHO, ICD coding, and almost every non-US hospital chart in centimeters. Switching between the two has one safe pattern. Combine into total inches first, then multiply.
```text
height_in = feet × 12 + inches
height_cm = height_in × 2.54
```
Worked example: 5'7" → `5 × 12 + 7 = 67 in` → `67 × 2.54 = 170.18 cm`. Reverse direction (cm to ft + in) for a US patient handed a metric chart: `175 cm ÷ 2.54 = 68.898 in → 5 ft + 8.898 in ≈ 5'8.9"`. Round to the nearest half-inch only at the very end. Skip the arithmetic entirely with the [length converter](/tools/length-converter), which handles inches to cm in either direction without intermediate rounding.
### Buying a TV: 55" diagonal vs wall width
A 55" TV is 139.7 cm on the diagonal, but its actual width on a 16:9 panel is `139.7 × 0.8716 = 121.76 cm`. Add 2 to 3 cm of bezel and the visible footprint is roughly 124 cm. Subtract that from the wall width and you want at least 20 cm of breathing room on each side, otherwise the TV looks crammed. Sound bars, console shelves, and side speakers eat the rest. The same arithmetic applies at every size: a 65" diagonal is `165.1 cm`, but the real width on the wall is `144 cm`.
### International apparel: EU 38, US 8 and the 81 cm waist
Apparel sizing is where centimeters to inches errors cost real money. Cross-border denim sells two ways: EU and Asian brands print waist in cm, US and UK brands print in inches. A "US 32" waist is `81.28 cm`, which European retailers usually round to `81` or `82`. Off-the-shelf shoes use the foot length directly: a Japanese or Chinese 27 cm shoe size is roughly US men's 9 (foot length plus 7 cm gives the US size). Get this wrong by one centimeter and the shoe goes back.
### CNC and 3D printing: why 0.39 kills tolerances
CNC mills and 3D printers run on tolerances of `±0.05 mm` for metal and `±0.2 mm` for FDM plastic. Speed-converting a 1000 mm part with `× 0.39` instead of `× 0.3937007874` produces an inch value that is 0.37 mm short over the full length. That already eats the metal tolerance budget on its own, before any machine-induced error. The rule for any path that ends up on a tool: divide by 2.54 directly, or carry the full factor `0.3937007874`. Anything else compounds. Keep a [length converter](/tools/length-converter) tab open while you read drawings. It carries the canonical 2.54 with no manual rounding.
### Cross-border e-commerce: DHL and FedEx box limits
DHL Express and FedEx International cap a single parcel at length + girth ≤ `419 cm (165 in)`, with a single longest dimension ≤ `274 cm (108 in)`. USPS Priority Mail International caps total length + girth at `108 in (274.32 cm)`. Hit the cap by 1 cm and the package is rejected at the warehouse, not the door. Some carriers also charge by dimensional weight using cm or in depending on origin country, so a `60 × 40 × 40 cm` box quoted in centimeters is not the same as `24 × 16 × 16 in` quoted in inches. The first is `27.5 in × 15.7 in × 15.7 in` and lands in a different fee bracket.
---
## Beyond cm and inches: mm, m, ft, yd in one chain
The `2.54` anchor unlocks the rest of the metric and imperial length family.
### mm to inches: sub-millimeter precision
`1 mm = 0.03937 in`, exact within the same 2.54 chain. Inversely, `1/64 in = 0.396875 mm`, which is the standard step on US machinist scales. Common engineering thicknesses:
| metric | imperial | typical use |
| ------ | ----------------- | ------------------- |
| 1 mm | 0.0394 in | thin sheet metal |
| 3 mm | 0.1181 in (≈ 1/8")| acrylic sheet |
| 6 mm | 0.2362 in (≈ 1/4")| plywood, plate glass|
| 10 mm | 0.3937 in | thick bar stock |
| 25.4 mm| 1.0000 in | the anchor |
The mm to inches lookup matters most when you order metric stock from a US supplier or vice versa. One decimal slip turns a 6 mm board into a 60 mm slab.
### m to ft: architecture and real estate
`1 m = 3.28084 ft`, and `1 ft = 0.3048 m` exactly. A standard US 8 ft ceiling is `2.4384 m`; a European 2.5 m ceiling is `8.20 ft`. Real-estate listings between continents always quote both, but the conversion factor is the same `2.54` chain underneath: `1 ft = 12 in = 12 × 2.54 cm = 30.48 cm = 0.3048 m`.
### yd and m: sports fields
`1 yd = 0.9144 m` exactly. That is the very definition that anchors the whole 1959 agreement. A 100 m sprint is `109.36 yd`; a 100 yd US football field is `91.44 m`. UEFA pitches are sized in metres, NFL fields in yards.
### Fractional inches: when decimals aren't enough
US woodworking, plumbing and machinist drawings still default to `1/16, 1/32, 1/64`. To convert cm to a fraction:
```text
in_decimal = cm × 0.3937007874
fraction = round(in_decimal × 64) ÷ 64 // nearest 1/64"
```
Worked example: `3 cm × 0.3937 = 1.1811 in → 0.1811 × 64 ≈ 11.59 → round to 12/64 = 3/16"`. So 3 cm ≈ `1 3/16 in`. Always reduce the fraction at the end (`12/64 = 3/16`).
### The whole length family at a glance
| 1 mile | = 1.609344 km exact |
| ------ | ------------------------ |
| 1 yd | = 0.9144 m exact |
| 1 ft | = 0.3048 m = 30.48 cm |
| 1 in | = 25.4 mm = 2.54 cm exact|
The same 1959 international agreement that pins inches to centimeters also defines the kilogram and pound chain. For the weight side of the same story, see our [kg to lbs conversion guide](/blog/kg-to-lbs-pounds-kilograms-conversion-guide). Volume runs on a different definition entirely, covered in the [ml to oz conversion guide](/blog/ml-to-fl-oz-fluid-ounces-conversion-guide). Temperature has its own three-unit chain in our [temperature conversion guide](/blog/temperature-conversion-celsius-fahrenheit-kelvin-guide). For a single-tab quick converter across all four families, the [weight converter](/tools/weight-converter), [volume converter](/tools/volume-converter) and [temperature converter](/tools/temperature-converter) sit alongside the length tool.
---
## Common mistakes to avoid
### Confusing cm with mm (the 10× error)
A "30 cm display" is a 12 in laptop screen; a "30 mm display" is a smartwatch face. Japanese product listings often quote dimensions in mm, European listings in cm, and machine translations sometimes drop the suffix entirely. When in doubt, run the result through an inches to cm sanity check. 30 mm → 1.18 in, too small for any laptop, so the source must have meant cm.
### Diagonal vs width / height (the screen trap)
Every monitor and TV size refers to the diagonal, not the width. A 27" monitor is `68.58 cm` corner-to-corner but only `59.78 cm` wide on a 16:9 panel. Confusing the two before drilling a wall mount is expensive. Width formula for any 16:9 screen: `diagonal × 0.8716`; height: `diagonal × 0.4903`.
### The 0.39 speed factor in an engineering context
`0.39` and `0.3937` look interchangeable but compound. Across 1 m of CNC tool path the gap is `0.7 mm`, which already eats most of a precision-class tolerance budget. The fix: never type `0.39` into a CAM file. Use the canonical 2.54 and divide, or use the [length converter](/tools/length-converter) to generate the value once and copy it across.
### Rounding mid-calculation
A height of 5'7" should be converted in one shot: `(5 × 12 + 7) × 2.54 = 170.18 cm`. Splitting it as `5 × 30.48 + 7 × 2.54 = 152.40 + 17.78 = 170.18 cm` happens to land on the same number here, but split rounding to fewer decimals (`5 × 30.5 + 7 × 2.5 = 152.5 + 17.5 = 170.0`) accumulates error fast. Rule: keep at least four decimal places in intermediate steps, then round at the very end.
---
## Code examples: JavaScript and Python
The same canonical factor that powers the [length converter](/tools/length-converter) drops directly into any codebase. Both snippets below include a roundtrip assertion to catch precision drift before it ships.
### JavaScript
```javascript
// 1959 International Yard and Pound Agreement: 1 in = 2.54 cm exactly
const CM_PER_INCH = 2.54;
const cmToInches = (cm) => cm / CM_PER_INCH;
const inchesToCm = (inches) => inches * CM_PER_INCH;
// Height split for medical / travel forms
const cmToFeetAndInches = (cm) => {
const totalInches = cmToInches(cm);
const feet = Math.floor(totalInches / 12);
const inches = +(totalInches - feet * 12).toFixed(1);
return { feet, inches };
};
console.log(cmToInches(30)); // 11.811023622047244
console.log(inchesToCm(67)); // 170.18
console.log(cmToFeetAndInches(170.18)); // { feet: 5, inches: 7 }
// Roundtrip sanity check — should match to ~15 sig figs
const back = inchesToCm(cmToInches(170.18));
console.assert(Math.abs(back - 170.18) < 1e-10, "cm roundtrip drift");
```
`CM_PER_INCH` is the single source of truth. Define it once and derive everything else; never copy `2.54` into a second file, because the day someone "fixes" one and not the other, you ship a unit-conversion bug.
### Python (pandas batch + roundtrip assert)
```python
import pandas as pd
CM_PER_INCH = 2.54 # exact, by 1959 international agreement
df = pd.DataFrame({"cm": [10, 30, 100, 170.18, 215.9]})
df["inches"] = df["cm"] / CM_PER_INCH
df["cm_back"] = df["inches"] * CM_PER_INCH
df["roundtrip_error"] = (df["cm"] - df["cm_back"]).abs()
assert (df["roundtrip_error"] < 1e-10).all(), "roundtrip drift detected"
print(df.round(4))
# cm inches cm_back roundtrip_error
# 0 10.00 3.9370 10.00 0.0
# 1 30.00 11.8110 30.00 0.0
# 2 100.00 39.3701 100.00 0.0
# 3 170.18 67.0000 170.18 0.0
# 4 215.90 85.0000 215.90 0.0
```
The `assert` is the load-bearing line. IEEE 754 double-precision floats round-trip to within machine epsilon (`~1e-15`); the `1e-10` threshold leaves headroom while still catching a typo like `0.394` instead of the full factor.
---
## FAQ
### How many cm in an inch exactly?
`1 inch = 2.54 cm`, exact by definition since the 1959 International Yard and Pound Agreement, not a rounded approximation. The inverse is `1 cm = 0.3937007874… in`, an infinite non-repeating decimal. For everyday work `0.39` is fine (1.6% error); for engineering, medical or customs work use the full factor or divide by 2.54.
### How do I convert cm to inches in my head?
Fastest method: halve the cm and subtract 20%. Example: 30 cm → `15 - 3 = 12 in` (exact 11.81, error 1.6%). Cleaner integers: multiply by 4 and shift one decimal. 50 cm → `200 ÷ 10 = 20 in` (exact 19.69). Both methods stay within 1.6% of the true value, fine for furniture, clothing and screens.
### What is 5'7" in cm?
`5'7" = 170.18 cm`. The clean way: combine feet and inches into a single inch count first (`5 × 12 + 7 = 67 in`), then multiply by 2.54 (`67 × 2.54 = 170.18 cm`). Quick references: 5'0" = 152.4 cm, 5'10" = 177.8 cm, 6'0" = 182.88 cm, 6'2" = 187.96 cm.
### How many inches is 30 cm?
`30 cm = 11.811 in`, more precisely `11.811024 in`. This shows up constantly because A4 paper is 29.7 cm tall (≈ 11.69 in), one centimeter shy of 30. Speed-converted as "about 12 inches" the error is 1.6%, which is acceptable for desk-organizer shopping but not for cabinet-making.
### What is the formula for cm to inches?
`inches = cm ÷ 2.54`. The reverse is `cm = inches × 2.54`. The factor `2.54` was set by the 1959 international agreement signed by the US, UK, Canada, Australia, New Zealand and South Africa, which fixed `1 yard = 0.9144 m` exactly; dividing by 36 inches per yard yields `1 in = 0.0254 m = 2.54 cm`.
### How do I convert mm to inches?
`inches = mm ÷ 25.4`, since `1 in = 25.4 mm` exactly. Common results: 6 mm = 0.236 in, 10 mm = 0.394 in, 25 mm = 0.984 in. Watch out for the cm vs mm confusion: a 30 mm board is `1.18 in` (less than a thumb-knuckle) while a 30 cm board is `11.8 in`, an order of magnitude apart.
### Why is the inch defined as exactly 2.54 cm?
The 1959 International Yard and Pound Agreement fixed the international yard at exactly `0.9144 m`. Since `1 yard = 36 inches`, that pegs `1 inch = 0.0254 m = 2.54 cm` by simple division. The agreement deliberately made the inch a metric quantity so that engineering tolerances could be specified once, in SI, and traced anywhere on the planet.
### How many inches is 100 cm?
100 cm equals 39.3700787402 inches when divided by the exact 2.54 factor. For everyday use, 100 cm is just under 39.4 inches, or about 3 feet 3.4 inches tall. Round to 39 inches only when precision below 0.4% does not matter.
### Is 1 cm bigger than 1 inch?
No, 1 inch is bigger than 1 cm. One inch equals 2.54 cm, so an inch is about 2.54 times longer than a centimeter. A common quick check: 1 cm fits inside a single inch about two and a half times.
### How do I convert cm to inches in Excel or Google Sheets?
Use the CONVERT function: `=CONVERT(A1, "cm", "in")` returns the inches equivalent of the cm value in cell A1, accurate to spreadsheet precision. Wrap with ROUND for cleaner output: `=ROUND(CONVERT(A1, "cm", "in"), 2)`. Both Excel and Google Sheets accept this formula identically.
---
### Code Minification Guide: CSS, JS & HTML Explained
URL: https://go-tools.org/blog/code-minification-guide-css-js-html
What code minification is, how minifying CSS, JS, and HTML works, and why minify and gzip/brotli are different. Learn the order and minify your code free.
# Code Minification Guide: CSS, JS & HTML Explained
Code minification removes characters that a machine doesn't need (whitespace, comments, line breaks) from your CSS, JavaScript, and HTML source, and rewrites verbose patterns into shorter equivalents. The behavior stays the same; the file just gets smaller and loads faster.
One thing to get straight up front: minification is not compression. Minify operates on your source code, stripping syntactic redundancy. Gzip and Brotli operate on the bytes in transit, encoding repeated patterns. They run at different stages and remove different kinds of redundancy, which is why you should still minify even when your server already serves Brotli. This guide explains why.
Want to compress something right now? Go straight to the [CSS minifier](/tools/css-formatter), the [JavaScript minifier](/tools/js-formatter), or the [HTML minifier](/tools/html-formatter); each one runs entirely in your browser. But understanding the mechanics is what lets you decide *where* to compress and *whether* you even need to do it by hand. The rest of this guide covers what minification does, how CSS, JS, and HTML each get minified, how minify stacks with gzip and Brotli, when your build tool already handles it, and how source maps keep minified code debuggable.
## What minification is (and what it is not)
Minification does two things. It deletes characters that carry no meaning for the parser, and it rewrites your source into a shorter form that means the same thing. The output is equivalent to a machine and nearly unreadable to a human. Nothing about how the code runs changes, only its surface.
That last point is the invariant to hold onto for the rest of this guide: minify only edits the surface of your source (whitespace, comments, identifier names, redundant syntax), never the behavior or output. It is the mirror image of formatting. Formatting adds whitespace to make code readable; minifying strips it to make code small. Both sit on the same "semantically equivalent" axis, just pointing in opposite directions.
People constantly confuse three operations that sound similar. This table sorts them out:
| Dimension | Format (beautify) | Minify | Compress (gzip/Brotli) |
|-----------|-------------------|--------|------------------------|
| What it changes | Adds whitespace, line breaks, indentation | Removes whitespace and comments, shortens syntax | Byte-level encoding of repeated patterns |
| Which layer | Source code | Source code | Transfer / storage |
| Still source code? | Yes (readable) | Yes (runnable, hard to read) | No (binary, must be decoded) |
| Who does it | Developer / editor | Build tool / minifier | Server + browser |
| Reversible? | Semantically | Semantically (behavior unchanged) | Fully (decompress restores the bytes) |
Format and minify live on one axis, the semantic-equivalence axis. Compression lives on a different one. A formatted file and a minified file are both valid source; a compressed file is a binary blob that has to be decoded before anything can run.
This is where a costly misconception creeps in: "my server already does gzip, so minifying is pointless." It isn't, and the numbers later in this guide show why. Minification and compression remove different redundancy, so doing one does not make the other redundant.
It helps to think about *why* the bytes a minifier removes exist in the first place. You write whitespace, comments, and descriptive names for yourself and your teammates, since they make code reviewable and maintainable. The machine that parses your CSS, runs your JavaScript, or builds your DOM ignores every one of them. Minification throws away the human-only material once the humans are done with the source. That's also why minification is a *production* concern and never a development one: you keep the readable version in your repository and ship the stripped-down version to browsers. The readable copy is the source of truth; the minified copy is a build artifact you can regenerate at any time.
## How CSS minification works
CSS is the gentlest of the three to minify because its grammar leaves little room for ambiguity. A minifier strips comments, collapses runs of whitespace into nothing, drops the final semicolon in each block, and removes spaces around `{`, `}`, `:`, and `;`. That alone clears most of the bytes.
CSS also allows a set of equivalence rewrites that no other language shares. A good minifier applies them safely:
- Shorten colors: `#ffffff` becomes `#fff`, and `#ff0000` collapses to `red` (or the reverse, whichever is shorter to write).
- Drop units on zero: `0px` becomes `0`, and `margin: 0 0 0 0` becomes `margin: 0`.
- Strip leading zeros: `0.5em` becomes `.5em`.
- Merge shorthands: four separate `margin-top`, `margin-right`, `margin-bottom`, and `margin-left` declarations fold into one `margin`.
- Combine rules: adjacent rules with identical selectors or declarations can be merged, and duplicate declarations dropped.
Every one of these keeps the rendered result identical, which is the boundary a compliant minifier never crosses. But CSS is order-sensitive: a later rule overrides an earlier one through the cascade. So a safe minifier will not blindly reorder rules that could change which declaration wins. Shrinking bytes is allowed; changing the cascade is not.
That constraint is more subtle than it sounds. Two declarations that look mergeable might not be, because something between them references the same property at the same specificity. Consider:
```css
.btn { color: #ff0000; }
.alert .btn { color: blue; }
.btn { color: #f00; }
```
The first and third rules share a selector and could merge, but only if doing so doesn't move the declaration past the middle rule in a way that changes which wins for an element matching both. A naive merge that reorders these could break the cascade. This is the kind of edge case a production-grade engine like CSSO is built to reason about, and it's why you shouldn't hand-roll your own "delete the whitespace" minifier with a regex. The transforms look mechanical, but the safety analysis behind them is not.
Our [CSS minifier](/tools/css-formatter) uses the CSSO engine for this kind of lossless minification, and it runs entirely in your browser with a byte-savings readout so you can see the payload impact of each pass. The same tool also formats in the other direction, so you can take a minified stylesheet you copied off a live site and expand it back into readable, indented rules. Reach for it when you've copied a snippet of CSS and want to check its compressed size, or when you're shipping a static page with no build step to do it for you.
## How JavaScript minification works
JavaScript minification goes much further than CSS, and that's where both the savings and the traps live. To see why, look at a small function before and after Terser:
```js
// before
function calculateTotal(items, taxRate) {
let runningTotal = 0;
for (const item of items) {
runningTotal += item.price * item.quantity;
}
return runningTotal * (1 + taxRate);
}
```
```js
// after
function calculateTotal(t,a){let n=0;for(const o of t)n+=o.price*o.quantity;return n*(1+a)}
```
The function name `calculateTotal` survives because it's exported (or could be called from elsewhere); the parameters and the loop variables collapse to single letters. That's the core of it, but a JS minifier does several distinct things:
- Identifier mangling: local variables and parameters get renamed to single letters, so `getUserPreferences` becomes `a`. Only locals are mangled; globals and exported names stay intact by default, because renaming them would break code that references them from outside.
- Dead-code elimination: unreachable branches and unused variables are removed, working alongside tree-shaking at the bundler level.
- Constant folding and syntax compression: expressions get shortened, so `true` becomes `!0`, `false` becomes `!1`, and `return undefined;` becomes `return;`.
The most important thing to know about JS minification is the automatic semicolon insertion (ASI) trap. JavaScript lets you omit semicolons, and the parser inserts them for you under specific rules. When a minifier deletes the line breaks those rules depend on, code can change meaning. The classic failure is a statement that begins with `(` or `[` getting silently glued onto the previous line:
```js
const x = getValue()
[1, 2, 3].forEach(handle)
```
Without semicolons, this parses as `getValue()[1, 2, 3]`, an indexing expression rather than two statements. Once minified onto one line, the bug is locked in. The same hazard appears with a line starting in `(`, where the previous expression gets called like a function. Modern Terser handles most real-world cases gracefully because it parses the code into an abstract syntax tree first and re-emits semicolons where they're needed, rather than doing blind text deletion. But bad source plus aggressive minification is a genuine source of production bugs, and the failures are nasty precisely because they only appear in the minified build, not in development. The fix is on your side: write code with explicit semicolons and unambiguous syntax, and the minifier stays safe. A linter rule or an auto-formatter that inserts semicolons at the source level removes the risk entirely.
A compliant minifier preserves behavior, but only if the input is valid, standard JavaScript. Terser parses ECMAScript; it does not understand TypeScript or JSX. Those have to be transpiled to plain JS first, otherwise minification fails at the parse step. If you paste a `.ts` file into a JS minifier and get an error, that's why.
One naming question comes up a lot: minify versus uglify. They mean effectively the same thing. "Uglify" comes from UglifyJS, the early popular JS minifier; Terser is its modern fork that supports ES2015 and later. Today "minify" is the generic term across all three languages, and "uglify" survives as an older, JS-specific name for the same process.
Our [JavaScript minifier](/tools/js-formatter) runs Terser in the browser, renaming locals, dropping dead code, and stripping comments, and reports how many bytes it saved on each pass.
## How HTML minification works
HTML minification starts with the basics: remove comments (keeping the `` declaration and any conditional comments you still rely on), collapse whitespace between tags, and trim redundant spaces inside attribute lists. A small fragment shows the shape of it:
```html
```
becomes:
```html
```
The comment is gone, the indentation between tags is collapsed, the optional `` closing tags are dropped, and the unquoted attribute values lose their quotes. From there a minifier can apply a few more HTML-specific tricks:
- Remove optional closing tags: the HTML spec allows omitting ``, ``, ``, and several others, so a minifier can drop them.
- Remove attribute quotes: when a value has no spaces or special characters, `class="x"` becomes `class=x`.
- Collapse boolean attributes: `disabled="disabled"` becomes just `disabled`, and `checked="checked"` becomes `checked`.
- Minify embedded CSS and JS: the contents of `
```
Apply `.preview-protanopia` to your page wrapper to see what a protanope sees. Repeat with the deuteranopia and tritanopia matrices (their coefficients are documented in the Brettel paper, and bundled in most CVD simulator libraries). Chrome DevTools' Rendering panel has the same simulators built in under "Emulate vision deficiencies", useful for quick checks, less useful for capturing screenshots in CI.
One rule sits beneath all the simulation work: never let color be the only difference between two states. Icons should differ in shape (`×` vs `✓`), states in text label ("Error" vs "Success"), categories in pattern (solid vs hatched). Color is a multiplier on those other signals, not a substitute.
There's a class of failures that contrast checkers don't catch but CVD simulators do. Pie chart segments distinguished only by hue, map legends that color-code countries, status pills that rely on a green/yellow/red gradient: all of them can clear WCAG 2 contrast against the background while being unreadable to a deuteranope reading them against each other. The rule is to check legibility *between adjacent colors in the design*, not just against the surrounding canvas. Two slate-500 segments next to each other at the same lightness are indistinguishable regardless of hue rotation. Add a luminance step between adjacent regions and the chart survives every CVD variant.
Achromatopsia and cone monochromacy are rare but worth designing for explicitly because they collapse all hue distinctions. A user with achromatopsia perceives only luminance; your color-coded UI looks like a grayscale photograph to them. If your design holds up when you run a `filter: grayscale(1)` over the whole page (try it in DevTools), you've passed the strictest version of the "color is not the only signal" rule. It's the cheapest accessibility test you can run, and it surfaces a surprising number of failures the moment you toggle it.
## Auditing Tailwind and Material palettes
Most front-end work uses a pre-built palette: Tailwind v4, Material 3, Radix, or shadcn. The accessibility question becomes "at which stop of this ramp does the text become readable?", and the answer is more rule-of-thumb than the docs admit.
For Tailwind v4's slate ramp against pure white, the WCAG and APCA numbers fall like this:
| Tailwind class | Approx hex | WCAG vs white | AA body | AAA body | APCA Lc (approx) |
| -------------- | ------------ | ------------- | -------- | -------- | ---------------- |
| slate-400 | `#94a3b8` | 2.56:1 | ✗ | ✗ | ~38 ✗ |
| slate-500 | `#64748b` | 4.76:1 | ✓ | ✗ | ~60 ✗ body |
| slate-600 | `#475569` | 7.58:1 | ✓ | ✓ | ~78 ✓ body |
| slate-700 | `#334155` | 10.35:1 | ✓ | ✓ | ~90 ✓ body |
The practical rules that fall out:
- **Body text needs slate-600 or darker.** Slate-500 passes WCAG AA but fails APCA's body-text threshold, so it's compliant but uncomfortable. Slate-600 is the safe floor.
- **UI labels and secondary text can use slate-500.** UI components only need 3:1; the 4.76:1 from slate-500 is comfortable, and the text usually carries supporting visual context.
- **Placeholders should use slate-400 or fade lighter.** Placeholder text is WCAG 1.4.3-exempt as decorative *only if* you keep a visible label above the field. Inline-label-as-placeholder patterns must hit the body-text threshold.
- **Pure black on slate-100 / slate-200 is wasted ink.** You're at 17:1+; consider using slate-700 or slate-800 as the text color for a softer feel without losing readability.
Material 3 uses a similar tonal palette structure. Its `surface-container-high` lives around tone 92 (very light); its `on-surface` lives around tone 10 (near-black). The contrast between any `on-surface` and any `surface-*` token in the same family is guaranteed by Material's spec to clear AA. Don't pair `on-surface-variant` (tone 30) against `surface-container` (tone 94) without checking; that's a real-world miss I've seen ship.
If you have an existing palette that fails contrast, OKLCH gives you the cleanest fix path. Instead of nudging hex codes blindly, convert your color to OKLCH, hold C (chroma) and H (hue) fixed, and reduce L (lightness) until the contrast passes. Because OKLCH's L channel is genuinely perceptual, the brand recognition stays intact while the contrast tightens. The [HEX to OKLCH tool](/tools/hex-to-oklch) does this conversion in one step; the [OKLCH explained](/blog/oklch-color-space-explained-tailwind-v4) sister post covers the math in depth.
Dark mode deserves its own treatment. WCAG 2's symmetric ratio passes the same combinations in light and dark mode by definition. APCA, being polarity-aware, frequently flags dark-mode body text as harder to read than the same hex pair would be in light mode. Light-on-dark always loses some perceived contrast relative to dark-on-light at the same numeric ratio; it's a known effect of how the eye adapts. Re-run APCA on every dark-mode pair before shipping.
## Contrast checkers and CI workflows
Designers and engineers each have a favorite checker; the practical question is which combination of tools you stitch into a real workflow. Here is the field as of 2026:
| Tool | WCAG 2 | APCA | CVD sim | Palette audit |
| ------------------------------- | ------ | ---------- | ----------- | -------------- |
| WebAIM Contrast Checker | ✓ | ✗ | ✗ | ✗ |
| Adobe Color | ✓ | ✗ | ✓ | ✓ |
| Stark (Figma plugin) | ✓ | ✓ | ✓ | ✓ |
| Polypane (browser) | ✓ | ✓ | ✓ | ✓ |
| Chrome DevTools color picker | ✓ | ✓ (exp.) | ✗ | ✗ |
| axe DevTools | ✓ | ✗ | ✗ | ✓ (page-level) |
| Go Tools Color Converter | ✓ | ✓ | ✓ (8 types) | ✓ (Tints/Shades) |
A workflow that holds up under real product pressure looks like this:
1. **In Figma**, designers run Stark on each frame to surface failing pairs early. Stark catches the obvious offenders before any hex code reaches the codebase.
2. **At hex-code handoff**, engineers paste the value into the [Color Converter](/tools/color-converter) to get WCAG ratio + APCA Lc + gamut classification + CVD preview in one row. If the pair is dark-mode or saturated-brand, the dual metric catches the WCAG-passes-APCA-fails cases Stark might miss.
3. **At PR time**, `axe-core/playwright` scans the built pages for any contrast violation on the rendered DOM, including dynamic states. This catches focus rings, hover states, and disabled affordances that static design files miss.
4. **In QA**, Chrome DevTools' Rendering tab simulates protanopia/deuteranopia/tritanopia for spot checks on critical flows. The Color Picker in DevTools also surfaces a WCAG ratio inline when you hover any element.
Pa11y, Lighthouse CI, and `@axe-core/playwright` all expose contrast assertions as part of their broader accessibility audits. None of them check APCA today; they all check WCAG 2. The realistic compromise is "enforce WCAG 2 AA in CI, sanity-check APCA Lc manually for brand colors and dark mode."
A pattern worth borrowing from larger design-system teams: bake the contrast check into your token validation step, not just into page-level QA. If your design tokens compile from a source file (JSON, YAML, or a TypeScript module), add a script that enumerates every `--text-*` × `--surface-*` pairing the system allows and asserts a minimum WCAG ratio. The script runs in milliseconds, catches regressions when someone tweaks a token value, and produces a contrast matrix that doubles as documentation for the design team. The check is independent of any rendered page; it operates purely on the tokens, so it catches the failure before any UI ships.
For ad-hoc conversions during this workflow (converting between hex, RGB, HSL, and OKLCH while you debug), the [HEX to RGB](/tools/hex-to-rgb), [HEX to HSL](/tools/hex-to-hsl), [HEX to OKLCH](/tools/hex-to-oklch), and [RGB to HEX](/tools/rgb-to-hex) spokes cover round-trips into and out of any color format your toolchain expects.
## Common mistakes and how to fix them
After years of accessibility audits, the same six failures keep showing up. Each has a tidy fix:
1. **Placeholder text in light gray.** `#999999` on white is 2.85:1, fails AA. Either deepen to `#666666` (5.74:1, passes AA) or, better, replace placeholder-as-label patterns with a persistent visible label above the field. Placeholders should not carry information.
2. **Brand orange button with white text.** `#FFA500` on white is 1.97:1, fails AA badly. The fix that preserves brand is to invert the contrast direction: dark text (e.g. `#451a03`) on the orange background, or keep the white text but darken the button to a deep saturated brown-orange. Verify in the [Color Converter](/tools/color-converter) before shipping.
3. **Bright blue link in dark mode.** `#3b82f6` on `#000000` is 5.71:1, a WCAG AA pass, but APCA Lc ~65, *below* the Lc 75 body threshold. Reach for OKLCH and bump L from ≈ 0.63 to 0.75, holding C and H fixed; you'll land near `#7aa5f8` with a comfortable APCA Lc 80+ and the same hue.
4. **Disabled text in `#CCCCCC`.** 1.61:1 against white. WCAG 1.4.3 exempts purely decorative text from the ratio rule, *but* disabled UI controls are not decorative; they communicate "this is currently unavailable." Pair the muted color with a non-color cue (strikethrough, lock icon, "Disabled" tooltip) so a CVD or low-vision user still understands the state.
5. **Status icons that differ only in hue.** A red `×` and a green `✓` is fine because the shape already distinguishes them. A red dot vs a green dot is not. Use shape and color together; the [Color Converter's](/tools/color-converter) CVD preview makes the failure case obvious in a second.
6. **Text over a gradient background.** A gradient that runs from `#3b82f6` to `#a78bfa` against white text passes contrast in the middle and fails near the lavender end. The fix is to enforce the contrast against the *worst-case* point of the gradient, or to overlay a semi-transparent dark scrim so the effective background luminance is always under a known threshold.
Each fix takes minutes. The audit cycle they avoid takes weeks.
## FAQ
### What is WCAG AA contrast ratio?
WCAG AA requires ≥ 4.5:1 between body text and background, or ≥ 3:1 for large text (≥ 18pt regular or ≥ 14pt bold) and UI components like form borders. AA is the legal baseline under ADA, EAA, and Section 508. Most commercial sites target AA because it covers the regulatory bar without forcing brand colors toward grayscale.
### What contrast ratio do I need for AAA?
WCAG AAA requires ≥ 7:1 for normal body text and ≥ 4.5:1 for large text. AAA is recommended for medical, educational, and government sites where the user base skews toward higher accessibility needs. Brand colors often need flattening toward grayscale-adjacent values to pass AAA, which is why many commercial products stop at AA.
### What is APCA and is it WCAG 3.0?
APCA (Advanced Perceptual Contrast Algorithm), designed by Andrew Somers / Myndex, is a candidate algorithm under the WCAG 3 Silver project. It uses polarity-sensitive Lc scores from -108 to +108 instead of symmetric ratios. WCAG 3 is still in early draft and APCA has not been formally ratified. WCAG 2.1 / 2.2 AA remains the regulatory standard you must hit today.
### Does dark mode help contrast accessibility?
Sometimes, but not automatically. WCAG 2's symmetric ratio passes the same combinations in light and dark mode, but APCA (which is polarity-sensitive) often flags dark-mode body text as harder to read than the same hex pair in light mode. Always re-test dark mode against both WCAG and APCA before shipping; light-on-dark loses perceived contrast in ways the symmetric ratio cannot see.
### Why does my brand color fail WCAG AA?
Saturated mid-luminance colors (most oranges, yellows, lime greens, light blues) have relative luminance values too close to white to clear 4.5:1. The fix: keep the brand hue for accents and large headlines, but pair body text with a darker tone from the same hue family. Use OKLCH to lower the L channel without shifting hue. The [Color Converter](/tools/color-converter) finds the closest passing shade in one step.
### Are WCAG 2 ratios and APCA scores compatible?
No. WCAG 2 returns a symmetric ratio (1–21); APCA returns a polarity-signed Lc score (-108 to +108). The relationship is non-linear: a pair that's 4.5:1 in WCAG might score Lc 60 or Lc 75 in APCA depending on which color is on top. Treat them as two independent checks, not as translations of one another.
### Can I use color contrast for small UI icons?
Yes, with caveats. WCAG 2.1 §1.4.11 requires ≥ 3:1 for UI components and graphical objects. For decorative icons paired with a visible text label, contrast requirements relax because the label carries the meaning. For stand-alone icons (e.g., a search magnifier with no label), enforce the full 3:1 against the surrounding background.
### How do I test color blindness without simulating?
Use Chrome DevTools → Rendering → "Emulate vision deficiencies" for protanopia, deuteranopia, tritanopia, and achromatopsia. Combine with the [Color Converter's](/tools/color-converter) 8-type CVD preview for the anomalous trichromacy variants (deuteranomaly being the most common at 5% of men). For audit reporting, capture screenshots under each simulation so reviewers can see the failure modes inline.
### Is 4.5:1 contrast ratio enough for accessibility?
Yes — for normal body text on standard commercial sites. The 4.5:1 WCAG contrast ratio is the AA threshold for body text under 18pt regular / 14pt bold. Government, medical, and educational sites should target AAA (7:1). Anything below 4.5:1 fails accessibility audits and ADA compliance baselines.
## Conclusion
Five takeaways carry the whole guide:
- **AA 4.5:1 is the legal floor.** Hit it for all body text or expect compliance noise.
- **AAA 7:1 is for healthcare, education, and government.** Most commercial brands stop at AA by design.
- **APCA Lc is the real-readability sanity check.** Run it in parallel with WCAG 2, especially for dark mode and saturated brand colors.
- **Color is never the only signal.** Pair every color cue with shape, text, or pattern. Deuteranomaly alone is 5% of male users.
- **OKLCH L is the right knob.** When a color fails contrast, reduce L (not S, not B) to fix it without drifting hue.
Drop any two hex codes into the [Color Converter](/tools/color-converter) to see WCAG ratio, APCA Lc, gamut classification, and the 8-type CVD preview side by side. That single view replaces six separate tools and is the fastest way to close out the audits this guide describes.
---
### Webhook Signature Verification Failed: Causes and Fixes
URL: https://go-tools.org/blog/webhook-signature-verification-failed-hmac-guide
Webhook signature verification failed? Usually it's the raw body, the digest encoding, or a missing timestamp prefix. Debug yours with a free HMAC tool.
# Webhook Signature Verification Failed? Find Your Cause
A webhook signature verification failed error means one thing: the digest your code computed does not equal the digest in the request header. That is the entire message. It says nothing about permissions or expiry, and it is almost never a bug in the provider's SDK. Something differs between the bytes the provider hashed and the bytes you hashed.
Four inputs decide the outcome: which bytes were signed, which key bytes were used, which hash algorithm ran, and which text encoding you compared in. Get any one wrong and the failure looks identical. The error carries no hint about which one it was, so the job is narrowing the input space rather than reading the message more carefully.
Pick a starting branch:
```
Signature doesn't match? Three branches:
├─ Did your framework parse the JSON before you saw it? → Section 3
├─ Does the header value carry a prefix, or look like base64? → Section 4
└─ Does the provider's header contain a timestamp? → Section 2
```
## 1. What a signature mismatch tells you
Verification is a comparison of two byte strings. When it fails, one of four things is wrong, and they are independent of each other.
**Which bytes got signed.** The provider hashed a specific sequence of bytes. Maybe that is the request body alone, maybe it is a timestamp glued to the front of the body. If your framework parsed the JSON and handed you an object, you no longer have those bytes and cannot reconstruct them reliably. This is Section 3, and it is the most common cause by a wide margin.
**Which key bytes got used.** The same secret string can be interpreted as UTF-8 text, as hex, or as base64, and each reading produces a different key. So does a secret with an extra newline the config loader kept. A second failure hides in this dimension: the secret may be the wrong secret entirely rather than the wrong reading of the right one, which is Section 6.
**Which encoding you compared in.** A digest is 32 raw bytes for SHA-256. Hex and base64 are two ways of writing those same bytes down as text, and they never look alike. Compare one against the other and you get a permanent hmac signature mismatch even though the underlying bytes agree.
**Which hash algorithm ran.** Most providers use SHA-256 and document it, so this dimension usually costs you nothing. GitHub is the exception worth knowing about: every delivery carries `X-Hub-Signature` (HMAC-SHA1) next to `X-Hub-Signature-256` (HMAC-SHA256), and GitHub's own docs say the SHA-1 header "is only included for legacy purposes" while recommending the 256 variant. Read the wrong one and the length gives it away before the bytes do. The body from Section 2, signed with the same secret under SHA-1, is `sha1=ba2954d180839d8170b08b32cd38483775aaae96` — 40 hex characters against the 64 of its SHA-256 digest.
Keep those four separated while you debug. The fastest way to isolate a dimension is to compute the digest outside your application from inputs you control: paste a body and a secret into the [HMAC generator](/tools/hmac-generator) and see what you get. It runs entirely in your browser and the secret never leaves the page, so a production signing secret is safe to paste into it. HMAC runs the same SHA-256 primitive as a plain [SHA-256 hash](/tools/sha-256-generator), just keyed with your secret, so if you can reproduce the provider's value by hand, the cryptography is fine and the bug is in your request handling.
## 2. What the four big providers actually sign
The assumption that sinks most integrations is that every provider signs the request body and nothing else. Two of the four biggest do not. What each one hashes, verified against the current provider documentation:
| Provider | Header | Signed string | Encoding | Value prefix | Secret | Timestamp tolerance |
|---|---|---|---|---|---|---|
| Stripe | `Stripe-Signature` | `{timestamp}` + `.` + rawBody | hex | `t=…,v1=…,v0=…` | endpoint signing secret (`whsec_` prefix) | 5 minutes (300 seconds) |
| GitHub | `X-Hub-Signature-256` | rawBody (no prefix) | hex | `sha256=` | webhook secret token | none (no timestamp sent) |
| Slack | `X-Slack-Signature` + `X-Slack-Request-Timestamp` | `v0:` + `{timestamp}` + `:` + rawBody | hex | `v0=` | signing secret | 5 minutes |
| Shopify | `X-Shopify-Hmac-SHA256` | rawBody | **base64** | none | **app client secret** (not a separate webhook secret) | none |
Those four happen to cover three orthogonal axes. The signed string is either the body alone or a timestamp concatenation, and even the separator differs: Stripe uses `.` while Slack uses `:`. The encoding is hex for three and base64 for one. The secret comes from a dedicated webhook credential for three, and from the app's client secret for Shopify, which is the detail people get wrong most often because there is a field labelled "webhook" in the admin UI that is not the thing you want.
The same body signed four ways with one secret:
```
body : {"id":42,"event":"user.created"}
secret : whsec_test_secret
ts : 1700000000
```
| Shape | Value |
|---|---|
| GitHub style | `sha256=09dd9fef34ca68915e1ba93eb7515cbc33e7e753806767f81abc6409480c846b` |
| Shopify style | `Cd2f7zTKaJFeG6k+t1FcvDPn51OAZ2f4GrxkCUgMhGs=` |
| Stripe style | `t=1700000000,v1=4b56de5a58122bab8ebbadbed663fbc17d810096d57498f5b24a72f5123b2375` |
| Slack style | `v0=3faf37337484c62dcd1a6c1ff308d1345c31291e4aac9b44554a99e8e35a1f9c` |
Read the first two rows together, because they are the same 32-byte digest written twice. Sixty-four hex characters, or forty-four base64 characters including padding. Nothing about the two strings suggests they are equal, which is why comparing across encodings produces a mismatch that survives every "but the secret is right" check you can think of.
The last two rows prove the other half of the point. Same body, same secret, same algorithm, and neither digest resembles the GitHub one, because the string being hashed now starts with a timestamp. Most reports of a Stripe webhook signature verification failed error come down to this row: the code hashed the body on its own and never prepended the `t` value and the dot. Reproduce all four in the [HMAC generator](/tools/hmac-generator) by editing only the message field and switching the output format, and the mechanism stops being abstract.
One practical consequence of the timestamp column: a Stripe or Slack digest is only valid for a few minutes, so you cannot capture a signature today and replay it in a test tomorrow. GitHub and Shopify signatures are stable forever, which makes them far easier to debug and also means you have to think about replay protection yourself.
## 3. The raw body problem
Most reports of webhook signature verification failed trace back to a framework that read and parsed the body before your handler ever saw it.
### Your framework already destroyed the bytes
Web frameworks are built to save you from parsing. That convenience is what breaks signature verification, because by the time your handler runs, the original bytes are gone.
`express.json()` reads the request stream, parses it, and replaces `req.body` with a JavaScript object. The stream is consumed and cannot be read again. In FastAPI, declaring a Pydantic model or a `dict` body parameter means the framework reads and parses before your function is entered. Rails populates `params` from the JSON body through a middleware that runs before your controller action. Spring's Jackson converter turns the body into your DTO class, and by default the underlying `HttpServletRequest` input stream can only be read once.
Nothing here is a bug. Every one of these is doing what it was configured to do. The problem is that a signature covers bytes, an object is not bytes, and turning the object back into bytes is a different operation from the one the provider performed.
### Why re-serializing sometimes works, and that's the trap
The usual advice is that re-serializing changes the bytes. That is incomplete, and the missing half is what makes this failure so hard to diagnose. Sometimes it changes nothing at all.
`JSON.stringify(JSON.parse(body)) === body`, measured across payload shapes:
| Payload shape | Bytes after round-trip | Change |
|---|:-:|---|
| `{"id":42,"event":"user.created"}` | **identical** | none, which is why local tests pass |
| `{"amount":1.0}` | changed | → `{"amount":1}` |
| `{"n":1e3}` | changed | → `{"n":1000}` |
| `{"id":12345678901234567890}` | changed | → `{"id":12345678901234567000}` (precision lost) |
| `{"name":"caf\u00e9"}` | changed | → `{"name":"café"}` (6 bytes become 2) |
| `{"a":1}\n` | changed | trailing newline swallowed |
| `{ "a" : 1 }` | changed | interior whitespace swallowed |
| `{"v":-0.0}` | changed | → `{"v":0}` |
| `{"p":0.1000000000000000055511151231257827}` | changed | → `{"p":0.1}` |
Look at the first row. A flat object with an integer and a short ASCII string round-trips byte for byte, so a parse-then-restringify verifier passes every test you wrote against a fixture like that. Then you deploy, and the first payload carrying a monetary amount of `1.0`, an ID beyond 2^53, or a customer name with an accent fails. Not all of them. Just those.
That is the mechanism behind "works locally, intermittent 401 in production", and it is considerably worse than a verifier that fails all the time. A verifier that always fails gets fixed in an hour. One that fails on 3% of events gets blamed on the provider, retried, escalated, and lived with for weeks. If your failure rate is somewhere strictly between zero and one hundred percent, this table is where to look first.
Key order is the cause people expect and the least likely one in practice, because `JSON.parse` preserves insertion order for string keys. Numbers and whitespace are the real culprits.
### Getting the raw body in each framework
Express, with the route-specific parser registered before the global JSON parser:
```js
const express = require('express');
const crypto = require('crypto');
const app = express();
// This route must be registered BEFORE app.use(express.json()).
// body-parser marks the request as parsed, so a later raw() silently yields {}.
app.post('/webhooks/github', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body; // a Buffer, not an object
const digest = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(raw) // hash the Buffer directly, no toString()
.digest('hex');
console.log('bytes:', raw.length, 'digest:', digest);
res.sendStatus(200);
});
app.use(express.json()); // every other route still gets parsed JSON
app.listen(3000);
```
If you cannot reorder middleware, keep a copy during parsing instead:
```js
app.use(express.json({
verify: (req, res, buf) => { req.rawBody = Buffer.from(buf); },
}));
```
FastAPI. Starlette caches the body, so `await request.body()` returns the original bytes even in a handler that also receives a parsed model:
```python
import hashlib, hmac, os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
@app.post("/webhooks/github")
async def github(request: Request):
raw = await request.body() # bytes, exactly as received
expected = "sha256=" + hmac.new(
os.environ["WEBHOOK_SECRET"].encode("utf-8"), raw, hashlib.sha256
).hexdigest()
received = request.headers.get("X-Hub-Signature-256", "")
if not hmac.compare_digest(expected, received):
raise HTTPException(status_code=401, detail="bad signature")
return {"ok": True}
```
Rails, where `request.raw_post` gives you the unparsed body as a string:
```ruby
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def shopify
raw = request.raw_post
digest = Base64.strict_encode64(
OpenSSL::HMAC.digest('sha256', ENV['SHOPIFY_CLIENT_SECRET'], raw)
)
unless OpenSSL.secure_compare(digest, request.headers['X-Shopify-Hmac-SHA256'].to_s)
return head :unauthorized
end
head :ok
end
end
```
Go, where you read the body yourself and must remember it is drained afterwards:
```go
func handler(w http.ResponseWriter, r *http.Request) {
raw, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "unreadable body", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, []byte(os.Getenv("WEBHOOK_SECRET")))
mac.Write(raw)
expected := mac.Sum(nil)
got, err := hex.DecodeString(
strings.TrimPrefix(r.Header.Get("X-Hub-Signature-256"), "sha256="))
if err != nil || !hmac.Equal(expected, got) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
// Unmarshal from raw, never from r.Body — it has no bytes left.
w.WriteHeader(http.StatusOK)
}
```
Spring, where asking for `byte[]` skips Jackson entirely:
```java
@PostMapping(path = "/webhooks/github", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity github(@RequestBody byte[] payload,
@RequestHeader("X-Hub-Signature-256") String header)
throws GeneralSecurityException {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = "sha256=" + HexFormat.of().formatHex(mac.doFinal(payload));
boolean ok = MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8),
header.getBytes(StandardCharsets.UTF_8));
return ok ? ResponseEntity.ok().build()
: ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
```
`ContentCachingRequestWrapper` is the alternative when a filter has to do the check and you cannot change the controller signature. It has a trap of its own: `getContentAsByteArray()` returns bytes only after something downstream has read the stream, so calling it before `chain.doFilter(...)` gives you an empty array.
## 4. Encoding mismatches: hex, base64, and the key itself
Three separate encoding decisions sit between your digest and the header value, and any of them can break the comparison on its own.
**The digest encoding.** HMAC-SHA256 output is 32 bytes. Written as lowercase hex it is 64 characters; written as standard base64 it is 44 including the `=` pad. The two rows from Section 2 are one digest written in both:
| Encoding | Characters | Same 32 bytes written as |
|---|:-:|---|
| hex | 64 | `09dd9fef34ca68915e1ba93eb7515cbc33e7e753806767f81abc6409480c846b` |
| base64 | 44 | `Cd2f7zTKaJFeG6k+t1FcvDPn51OAZ2f4GrxkCUgMhGs=` |
A quick heuristic when you are staring at an unfamiliar header: if the value is 64 characters of `0-9a-f`, it is hex. If it is 44 characters ending in `=`, or contains `+`, `/`, or uppercase letters, it is base64. When you want to confirm rather than guess, run the base64 value through the [Base64 decoder](/tools/base64-decode-encode) and check that it yields 32 bytes; if it does, both strings describe the same digest and you were comparing text formats, not signatures.
**The value prefix.** GitHub sends `sha256=` in front of the hex. Slack sends `v0=`. Stripe wraps everything in a comma-separated list of `key=value` pairs. None of those characters are part of the digest, so either strip the prefix from the header or add it to your own value. Doing neither is the most common reason an otherwise correct implementation reports an hmac signature mismatch, and in Node it does not even report a mismatch, as Section 7 explains.
**The key encoding.** The secret is bytes too, and the same string read as UTF-8, hex, or base64 gives three different keys. Providers that hand you a text token like `whsec_...` want UTF-8, but plenty of internal systems distribute base64 or hex secrets that must be decoded before signing. This failure mode is identical in shape to the JWT version of the problem and is covered in depth in [JWT invalid signature: every cause and how to fix it](/blog/jwt-invalid-signature-troubleshooting-guide), including how to tell whether a given secret is base64 or plain text.
## 5. Timestamp, tolerance, and replay windows
You can compute a digest that matches perfectly and still be rejected. Providers that include a timestamp expect you to check it, and a stale timestamp is a valid signature you must refuse anyway.
| Provider | Where the timestamp lives | Window |
|---|---|---|
| Stripe | `t=` inside `Stripe-Signature` | 5 minutes (300 seconds) |
| Slack | `X-Slack-Request-Timestamp` header | 5 minutes |
| GitHub | not sent | not applicable |
| Shopify | not sent | not applicable |
Both directions of getting the window wrong hurt. Too generous, and a captured request stays replayable for as long as you allow, which defeats most of the point of checking the timestamp. Too tight, and ordinary clock drift starts rejecting real deliveries. Five minutes is what both providers chose, and copying that is a sound default.
Before you widen a tolerance, check the clock. Container images do not run NTP, and a VM resumed from a snapshot can be minutes behind wall time with nothing in the logs to say so. A host that drifts steadily produces failures that begin as occasional and become total, which reads like a code regression and is not one.
The other clock bug is a unit mismatch. Every provider in the table sends epoch seconds. Compare one against a millisecond value like JavaScript's `Date.now()` and the difference is roughly a thousand times the real age, so every event is outside every plausible window. The symptom is a tolerance check that rejects one hundred percent of deliveries while the digest itself matches. If you are unsure which unit you are holding, the length is the tell, and [epoch seconds versus milliseconds](/blog/unix-timestamp-guide-epoch-seconds-ms-timezone-dst) covers the conversions and the timezone traps around them.
Use the raw timestamp string from the header when you build the signed string, not a parsed and reformatted number. Parsing `1700000000` to a float and printing it back can yield `1700000000.0`, and that is a different byte sequence.
## 6. Wrong secret, and secrets that rotate
Before you go any further into encodings, rule out the plainest cause: the secret may not be the right secret. Stripe's docs are explicit that "Stripe generates a unique secret key for each endpoint," and that if you point the same URL at both test and live keys, "the secret is different for each one." Three versions of one mistake follow from that.
Test mode and live mode hold separate secrets, so a value copied while the dashboard was in test mode fails every live delivery. Each endpoint holds its own, and the docs add that "if you use multiple endpoints, you must obtain a secret for each one you want to verify signatures on" — aim two endpoints at one handler with one secret in the environment and half your traffic fails. And `stripe listen` prints a signing secret for the CLI's local forwarding, which is a separate endpoint from anything registered in the dashboard, so the two are not interchangeable.
None of these look like encoding bugs from the outside. The digest is well formed, the comparison is correct, and the value in your environment is a real Stripe secret — just not the one that signed this delivery.
Rotation is the same dimension moving under you. It looks least like an encoding problem and gets misdiagnosed as a code bug most often. Nothing in your code changed, verification worked yesterday, and now a fraction of events fail.
The overlap window is deliberate. Stripe keeps the old endpoint secret valid for up to 24 hours after you rotate, and during that period the `Stripe-Signature` header carries one `v1` signature for each active secret. Shopify goes the other way: after rotation it can take up to an hour before it starts using the new secret to compute digests, so the old one is what you need in the meantime.
The Stripe behaviour is what breaks code, because the header looks like it has one signature in it. Splitting on `,` and taking the first `v1` you find works right up until there are two, at which point you match roughly half the time depending on which secret signed which event. Iterate over all of them:
```js
const crypto = require('crypto');
function verifyStripe(header, rawBody, secret, toleranceSec = 300) {
let t = null;
const v1 = [];
for (const pair of header.split(',')) {
const idx = pair.indexOf('=');
const key = pair.slice(0, idx);
const value = pair.slice(idx + 1);
if (key === 'v1') v1.push(value);
else if (key === 't') t = value; // keep the original string
}
if (t === null || v1.length === 0) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(t));
if (!Number.isFinite(age) || age > toleranceSec) return false;
const signedPayload = Buffer.concat([Buffer.from(`${t}.`, 'utf8'), rawBody]);
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest();
return v1.some((sig) => {
const received = Buffer.from(sig, 'hex');
return received.length === expected.length &&
crypto.timingSafeEqual(received, expected);
});
}
```
Two details in there matter beyond the loop. The timestamp goes into the signed payload as the string it arrived as, and the body is concatenated as bytes rather than through template interpolation, which would decode it as UTF-8 first.
The same shape applies when you rotate on your side: accept both the old and the new secret for the length of the overlap, then drop the old one. Whatever you rotate to needs full entropy, so generate it rather than typing it, using something like the [signing secret generator](/tools/jwt-secret-generator) for a 256-bit random value.
## 7. Comparing signatures without leaking timing
Once you have two digests, how you compare them is a security decision. String equality returns as soon as it finds a differing byte, so the time it takes reveals how many leading bytes were correct. An attacker who can submit many requests uses that to recover a valid signature one byte at a time. It is slow and noisy over the internet, and entirely practical on a local network.
Every runtime ships a fixed-time comparison:
| Language | Constant-time compare | When lengths differ |
|---|---|---|
| Node | `crypto.timingSafeEqual(a, b)` | **throws** |
| Python | `hmac.compare_digest(a, b)` | returns `False` |
| Go | `hmac.Equal(a, b)` | returns `false` |
| PHP | `hash_equals($known, $user)` | returns `false` |
| Ruby | `OpenSSL.secure_compare(a, b)` | returns `false` |
That last column is where a whole class of confusing incidents comes from. Node is the outlier, and it does not fail politely:
```
RangeError [ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH]: Input buffers must have the same byte length
```
It fires on a trivial slip. A hex SHA-256 digest is 64 characters. The value in `X-Hub-Signature-256` is 71, because `sha256=` is seven characters. Forget to strip the prefix and the two buffers have different lengths, so `timingSafeEqual` throws instead of returning false. Uncaught, that exception propagates out of your handler and Express turns it into a 500.
From the outside that looks like something else entirely. You are looking for a webhook 401 unauthorized response, you get a server error, so you go read your handler and your event dispatcher. The bug is one line above the comparison. Comparing a 64-character hex digest against a 44-character base64 one throws for the same reason, which means an encoding mismatch in Node also surfaces as a 500 rather than a clean rejection.
The fix is to check the length yourself and return false:
```js
function safeEqualHex(receivedHex, expectedHex) {
const a = Buffer.from(receivedHex, 'hex');
const b = Buffer.from(expectedHex, 'hex');
if (a.length !== b.length) return false; // guard before the call
return crypto.timingSafeEqual(a, b);
}
```
Leaking the length is harmless; a digest length is fixed by the algorithm and public. What you must not leak is which prefix matched. The Verify tab of the [HMAC generator](/tools/hmac-generator) folds the length difference into the same constant-time accumulator instead of returning early, so a length mismatch comes back as a plain false rather than an exception, and you can check a header value against your computed digest without writing throwaway code.
## 8. When the transport layer changed your bytes
You have ruled out the signed string, the raw body, the encodings, the clock, and rotation. What is left is the possibility that the bytes arriving at your process are not the bytes that left the provider.
**Compression.** A provider or proxy may send the body gzipped with `Content-Encoding: gzip`. The signature covers the uncompressed payload, so you must hash after decompression. Some frameworks decompress transparently and some hand you the compressed bytes, and a body that looks like binary garbage in your log is the giveaway.
**Chunked transfer.** With `Transfer-Encoding: chunked` there is no `Content-Length`, and code that trusts that header to size a read buffer truncates the body. The digest of a truncated body is valid nonsense: it will never match, and nothing looks wrong.
**Proxies and WAFs.** Any layer that reads and rewrites the body can change it. AWS API Gateway can base64-encode the body before it reaches a Lambda, so you must decode before hashing. Application load balancers, service meshes, and web application firewalls can all normalize or re-encode a payload on the way through. Test by comparing the byte length your handler sees against the `Content-Length` the provider sent.
**Character encoding and BOM.** Payloads can contain non-ASCII characters, and GitHub's documentation is explicit that the payload must be handled as UTF-8. Decoding the body to a string in the wrong charset and re-encoding it destroys every multi-byte character. A UTF-8 byte order mark, `EF BB BF`, prepended by a well-meaning editor or serializer adds three bytes that were never signed.
**Line endings and stray whitespace.** A body that crossed a text-mode file boundary can arrive with `LF` rewritten to `CRLF`. Read the provider's spec for the exact signing string too: some append a character of their own, and Typeform documents a trailing newline as part of what gets hashed. When a provider's docs mention any extra character, take it literally.
## 9. A repeatable debugging workflow
Run these in order. Each step either finds the bug or eliminates a branch, and stopping early is the point.
1. **Log the raw bytes before any middleware runs.** Write the body to a file, or log its byte length plus its SHA-256, from the earliest point in the request lifecycle you can reach. Length alone resolves a surprising number of cases: a value one greater than expected is a trailing newline, three greater is a BOM.
2. **Compute the digest by hand.** Paste those exact bytes and your secret into the [HMAC generator](/tools/hmac-generator), pick SHA-256, and set the output format to match the header. Doing this before anything else splits the problem cleanly in two.
3. **Compare the hand-computed value with the header.** Equal means the bytes and the secret are both correct and the bug is somewhere in your code path, so go read your comparison. Not equal means one of the inputs is wrong, so continue.
4. **Check the signed string against the table in Section 2.** Does this provider prepend a timestamp? With which separator? Add the prefix in the tool and recompute.
5. **Switch the digest encoding.** Recompute as hex and as base64 and compare both against the header. A 44-character header value with an `=` on the end is base64, whatever your code assumed.
6. **Switch the key encoding.** Try the secret as text, then hex, then base64. One of the three usually produces a match, and that tells you what the provider expects.
7. **Check the clock and the rotation state.** Compare your server's time against a known source, confirm you are handling epoch seconds, and check the provider's dashboard for a rotation in the last 24 hours.
Two habits make this loop much faster. First, capture one failing payload and work from it offline instead of waiting for the next delivery. Second, replay that captured body against your endpoint with a fixed signature so the input never varies between attempts. The [cURL command builder](/tools/curl-builder) assembles the request with the exact headers and a body read from a file, which keeps the bytes stable across runs. Reproducing the failure on demand is what turns an intermittent webhook signature verification failed report into a five-minute fix.
If you still need to file a support ticket, include the byte length of the body you hashed, the header value verbatim, the signed string construction you used, and the digest encoding. Never include the secret itself.
## FAQ
### Why does my webhook signature work locally but fail in production?
Your test payload probably survives a JSON round-trip unchanged, so re-serializing it is harmless. Real payloads contain floats, large integers, Unicode escapes, or extra whitespace, and those do change the bytes. Sign the raw body instead of a re-serialized copy; the table in Section 3 shows which shapes break.
### Should I include the sha256= prefix when comparing signatures?
Strip it, or add it to your own value so both strings match exactly. Your computed hex digest is 64 characters and the header value is 71 with the prefix. Some comparison functions return false on a length mismatch, and Node's `timingSafeEqual` throws instead of returning false.
### Can I verify the signature after my framework parsed the JSON?
Not reliably. Re-serializing reproduces the original bytes only for payloads with no floats, no integers beyond 2^53, no Unicode escapes, and no extra whitespace. The moment one appears the digest changes, so verification passes in testing and fails on a fraction of production events.
### Why do Stripe and GitHub produce different signatures for the same payload?
Because they hash different strings. GitHub signs the raw body alone. Stripe signs the timestamp, a literal `.`, then the body, so one payload delivered at two different times yields two different digests. Slack prepends `v0:` and its own timestamp. Same algorithm, different input.
### How long should the timestamp tolerance be?
Five minutes is what Stripe and Slack use, and copying it is a reasonable default. Shorter windows reject legitimate deliveries as soon as your server clock drifts. Longer windows widen the period in which a captured request can be replayed. Sync clocks with NTP before loosening the tolerance.
### Does timingSafeEqual return false when the lengths differ?
No. Node throws `RangeError [ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH]: Input buffers must have the same byte length`. Uncaught, that becomes a 500 instead of a 401, which sends you debugging your handler rather than the line above the comparison. Compare lengths first and return false yourself.
### My provider rotated the secret, so why do some webhooks still fail?
Rotation windows overlap. Stripe keeps the old secret valid for up to 24 hours and sends one `v1` signature per active secret, so code that reads only the first `v1` fails on roughly half the events. Shopify can take up to an hour to start using the new secret.
## Conclusion
Verification is a byte comparison, so webhook signature verification failed always resolves to a disagreement about bytes rather than to anything cryptographic. Keep the three dimensions apart while you debug. Capture the raw body before any parser touches it, and never hash a re-serialized object: it matches often enough to pass your tests and not often enough to work in production. Read the secret the way the provider reads it, since text, hex and base64 readings of one string give three different keys. Then check the encoding you compared in, because hex is 64 characters and base64 is 44 and both can describe the same 32 bytes.
After those, in rough order of likelihood: the timestamp prefix, the value prefix, the tolerance window, the rotation overlap, and the transport layer. However you write the comparison, guard the length first, then hand both values to your runtime's constant-time function.
When you want a value you can trust to compare against, compute it outside your application: paste the body and the secret into the [HMAC generator](/tools/hmac-generator) and let it tell you which side is wrong.
---
### WebP vs AVIF vs JPEG: Which Image Format Wins in 2026?
URL: https://go-tools.org/blog/webp-vs-avif-vs-jpeg-image-format-guide
AVIF is 20–30% smaller than WebP and 30–50% smaller than JPEG, but encodes 5–20× slower. 2026 browser support, real benchmarks, and fallback patterns. Try free.
# WebP vs AVIF vs JPEG: Which Image Format Wins in 2026?
**TL;DR.** AVIF is 20–30% smaller than WebP and 30–50% smaller than JPEG. WebP is roughly 25–35% smaller than JPEG. AVIF encodes 5–20× slower than WebP, while WebP decodes the fastest of the three. The 2026 winning workflow: serve AVIF first, fall back to WebP, keep JPEG as the universal safety net, and let the browser pick via the `` element.
That answer covers most readers. The interesting question is when *not* to reach for AVIF. Skip it on tight CI budgets where encoding time matters more than bytes. Hold it back for real-time user uploads, since browser-side AVIF encoding is still patchy. Keep JPEG primary if you must support iOS 16.0–16.3 or older Edge in the wild. Everywhere else, AVIF wins on file size, provided your `` markup is correct and your CDN sends the right MIME type.
The rest of this guide is the working detail: 2026 support numbers, a real-photo benchmark, a use-case decision tree, copy-paste `` patterns, conversion commands, and the four traps that cost teams time. If you just want the conversion done, our [free image compressor](/tools/image-compressor) handles JPEG, PNG, WebP, and AVIF in the browser without uploading anything.
## 1. 2026 browser support: WebP at 97%, AVIF at 93%
Two and a half years after Edge shipped AVIF, the support gap has narrowed to a few percent of global traffic. The question is no longer "is it supported" but "what do we serve to the last 3%?"
### 1.1 WebP: the safe default
WebP shipped in Chrome back in 2012, Firefox 65 (2019), Edge 18 (2018), and Safari 14 (2020). As of May 2026, caniuse puts global support at roughly 97%. WebP has graduated from "modern alternative" to "viable fallback." If you serve a single non-JPEG format, WebP is it.
### 1.2 AVIF: now usable as the primary format
AVIF arrived in Chrome 85 (August 2020) and Firefox 93 (October 2021). Safari 16.4 enabled it on macOS and iOS in March 2023. Edge was the laggard, only adding decode support in 121 (January 2024). Global coverage in May 2026 is roughly 93–95%.
One sharp edge worth flagging: iOS 16.0–16.3 has a known AVIF decode bug that can crash Safari on certain images. Those builds are still alive on devices held back by enterprise MDM, so AVIF without a working WebP or JPEG fallback is a real outage risk. Treat it as "primary with insurance," not "primary alone."
### 1.3 Quick compatibility matrix
| Format | Global support (May 2026) | Key limitation |
|--------|---------------------------|----------------|
| JPEG | 100% | Largest files; no transparency; no HDR |
| WebP | ~97% | No HDR or 10-bit color |
| AVIF | ~93–95% | Slow encode; iOS 16.0–16.3 decode bug; needs Edge 121+ |
## 2. Core differences: compression, speed, features
### 2.1 Compression efficiency on a real photo
One same-source benchmark. A 4000×3000 landscape photograph, original PNG around 24 MB, re-encoded at perceptually matched quality:
| Encoding | Size | Vs JPEG baseline |
|----------|------|------------------|
| JPEG q75 (mozjpeg) | ~2.1 MB | 0% (baseline) |
| WebP q75 (libwebp) | ~1.4 MB | 33% smaller |
| AVIF q60 (libavif, cpu-used 4) | ~1.0 MB | 52% smaller |
AVIF q60 here is visually equivalent to JPEG q80 and WebP q75. Quality scales are not interchangeable across codecs. Re-encoding "JPEG q75 to AVIF q75" is the classic beginner mistake; you end up with an oversized AVIF that looks identical to the source.
### 2.2 Encoding speed: the 5–20× tax
AVIF compresses harder because it does more work. Using libavif at the default `cpu-used 4`, a 4000×3000 image takes 5–20× longer than libwebp and roughly 50× longer than mozjpeg. That cost compounds across CI builds and any pipeline that re-encodes thousands of assets, including Lambda cold starts.
Two escape valves. `cavif --speed 9` (or libavif `cpu-used 9`) gets within 3× of libwebp at the price of 5–8% larger files. And cache aggressively: a content-addressed asset pipeline that skips re-encoding unchanged sources turns a slow codec into a one-time cost.
### 2.3 Color depth and HDR
JPEG and WebP top out at 8-bit sRGB. AVIF handles 10- and 12-bit color, Rec. 2020, DCI-P3, and PQ/HLG transfer functions natively. For HDR video thumbnails, pro photo galleries, or browser-side print proofs, AVIF is the only standardized option today.
### 2.4 Transparency and animation
JPEG has neither. WebP and AVIF both support alpha and animation. For transparent-PNG replacement specifically, AVIF encodes alpha more compactly: a UI illustration that shrinks 30–40% as WebP often shrinks 50–70% as AVIF.
## 3. Decision tree: which format for which job
### 3.1 Static sites: blogs, marketing pages, docs
AVIF primary, WebP fallback, JPEG safety net. Encoding happens once in CI, so the 5–20× tax is paid in build time, not user time. This is the canonical case for the three-source `` pattern in section 4.
### 3.2 User uploads: avatars, UGC, form attachments
Compress to WebP in the browser, re-encode to AVIF async on the server. Browser-side `canvas.toBlob('image/avif')` works only on Chrome 99+ today, so AVIF can't be the upload path. WebP compresses fast in the browser and saves bandwidth on the upload itself.
For a deeper comparison of client-side libraries (Squoosh, browser-image-compression, Compressor.js) and how they pair with server-side Sharp or Imagemin, see the sister [browser-based image compression guide](/blog/image-compression-browser-vs-node). Format choice and processing location are orthogonal; this guide covers the format axis.
### 3.3 HDR and high-fidelity photography
AVIF only. Nothing else on the open web stack supports 10- or 12-bit color today. Skip the fallback for HDR-only assets, or accept that the JPEG fallback will be SDR.
### 3.4 Legacy-browser-heavy audiences
JPEG primary, WebP optional, no AVIF. Government portals, certain East Asian enterprise environments, and B2B tools with long device tails sometimes show 5–10% IE/old-Edge traffic in analytics. WebP is now safe; AVIF is not yet.
### 3.5 Real-time CDN delivery
Server-side libvips or Sharp streaming WebP, with AVIF generated on a background worker and served on cache hit. Don't block the response on AVIF encoding. Cloudflare Polish, Vercel Image Optimization, and Cloudinary `f_auto` automate the pattern.
## 4. The `` fallback, done right
The `` element exists exactly so the browser can choose the best supported source. Three layers, listed AVIF first, give you the optimal byte budget without breaking on older clients.
### 4.1 The three-layer baseline
```html
```
Three things to call out. The browser walks `` elements top-down and picks the first one whose `type` it understands; everything else is ignored, not downloaded. The ` ` tag is the actual rendered element and inherits attributes (alt, sizes, classes) regardless of which source wins. And `width`/`height` on the ` ` are not optional in 2026; they reserve space and prevent Cumulative Layout Shift.
For above-the-fold images, swap `loading="lazy"` for `loading="eager" fetchpriority="high"`. Lazy-loading the LCP image is one of the most common Core Web Vitals foot-guns.
### 4.2 Responsive plus format: the full pattern
When you also need responsive resolutions, repeat `srcset` inside each ``:
```html
```
Yes, that's nine generated files per image. A build step is non-negotiable at this scale; section 5.3 covers what to plug in.
### 4.3 Server-side `Accept` negotiation as an alternative
If your CDN supports it, content negotiation collapses the markup to a single ` ` tag. The browser sends `Accept: image/avif,image/webp,image/apng,*/*` and the CDN responds with the best supported format at the same URL.
Cloudflare Polish, Vercel Image Optimization, Cloudinary `f_auto`, and CloudFront with Lambda@Edge all implement this. The trade-off: smaller HTML and one URL per image, but CDN lock-in and harder local debugging. A useful split is CDN negotiation for marketing pages, explicit `` for product UI where deterministic behavior matters.
## 5. How to convert images between formats
### 5.1 In the browser, no upload
The fastest path for a one-off or small batch is browser-only. Drop a JPEG into the [free image compressor](/tools/image-compressor), pick WebP or AVIF as the output, and the file never leaves your machine. AVIF input is supported everywhere; AVIF output uses native browser encoding on Chrome 85+ and transparently falls back to WebP on browsers that can't encode AVIF yet.
### 5.2 Command line for build pipelines
For a production pipeline, you want deterministic output and reproducible builds. These three commands are real:
```bash
# AVIF: cavif (Rust, easy install via cargo or brew)
cavif --quality 60 --speed 4 input.jpg -o output.avif
# WebP: cwebp (official Google libwebp)
cwebp -q 75 -m 6 input.jpg -o output.webp
# Both, plus resize, via libvips (fastest for batches)
vips webpsave input.jpg output.webp[Q=75,effort=6]
vips heifsave input.jpg output.avif[Q=60,compression=av1,effort=4]
```
`cavif --speed` runs 0 (slowest, smallest) to 10 (fastest, largest). Default 4 is the sweet spot for nightly builds; bump to 9 for PR previews where speed beats bytes.
### 5.3 Build pipeline integration
Most static-site frameworks already wrap these encoders. Pick the one that matches your stack:
```js
// Next.js — next.config.js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 828, 1080, 1200, 1920, 2400],
},
};
```
```ts
// Astro — astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
image: {
service: { entrypoint: 'astro/assets/services/sharp' },
},
});
```
```js
// Programmatic — Sharp (Node.js)
import sharp from 'sharp';
await sharp('input.jpg')
.resize({ width: 1600 })
.avif({ quality: 60, effort: 4 })
.toFile('output.avif');
await sharp('input.jpg')
.resize({ width: 1600 })
.webp({ quality: 75, effort: 6 })
.toFile('output.webp');
```
For tiny assets (icons under about 5 KB, inline avatars, email headers), skip the network entirely and inline as a data URI using [base64 encoding](/tools/base64-decode-encode). That swaps an HTTP request for a few extra bytes of HTML, usually a win below ~5 KB.
If you're picking between client-side and Node-side compression libraries (Sharp vs Squoosh vs browser-image-compression), the [browser-based image compression guide](/blog/image-compression-browser-vs-node) goes deeper on benchmarks and trade-offs.
## 6. Pitfalls and misconceptions
### 6.1 "AVIF always wins" — not for screenshots and line art
AVIF's strengths are continuous-tone photographs. On screenshots with sharp text, UI captures, and pixel art, WebP often produces smaller files at equivalent quality. AVIF's deblocking can introduce subtle banding on flat color regions. Run conversions both ways and pick the winner per asset class.
### 6.2 "Lossless WebP perfectly replaces transparent PNG" — close, not identical
WebP lossless is genuinely smaller than PNG, typically about 26%. The catch: "lossless" applies to the compressed image, but the encoder may still alter alpha-channel rounding in high-gradient regions. For pixel-exact reproduction (medical imagery, archival assets, anything legally required to match the source), keep PNG. For everything else, WebP lossless is a net win.
### 6.3 "Just upload .avif files" — your CDN may not know what they are
Older nginx and Apache configs predate AVIF. If `/etc/nginx/mime.types` doesn't list `image/avif avif;`, nginx serves AVIF as `application/octet-stream`. The browser sees the wrong Content-Type, refuses to render the image, and quietly falls back to JPEG, defeating the entire optimization. Curl your asset URL after deploy and check the Content-Type header. Five seconds of paranoia saves a week of "why is AVIF broken in production."
### 6.4 The iOS 16.0–16.3 AVIF crash
Some AVIF files trigger a Safari decode crash on iOS 16.0–16.3. The bug is fixed in 16.4 (March 2023), but enterprise MDM and slow OEM updates keep older devices alive. Mitigation: never ship AVIF without a working WebP source in the same ``, and never set an AVIF as the ` ` `src` directly. Following section 4's patterns already protects you.
## 7. The 2026 cheat sheet
| Scenario | Primary | Fallback | Encoder |
|----------|---------|----------|---------|
| Static marketing pages | AVIF q60 | WebP q75 + JPEG q80 | cavif + cwebp in CI |
| Blog posts and docs | WebP q75 | JPEG q80 | [free image compressor](/tools/image-compressor) (manual) or Sharp (build) |
| User uploads | WebP (client-side) | JPEG (browser without WebP encode) | browser-image-compression + server-side Sharp for AVIF |
| HDR / pro photography | AVIF 10-bit q70 | (none — drop SDR fallback or skip AVIF entirely) | cavif + libavif HEIF mode |
| Real-time CDN delivery | Negotiated (AVIF or WebP) | JPEG | Cloudflare Polish, Cloudinary `f_auto`, Vercel Image |
Two reminders before shipping. Always set `width` and `height` on the ` ` to prevent CLS. Always verify the production Content-Type header on at least one AVIF response; broken MIME config silently kills AVIF in production more often than any other failure on this list.
## FAQ
### Do I still need a JPEG fallback in 2026?
Yes. AVIF reaches roughly 93% of global users and WebP about 97%. That leaves a small but real population (old Edge, Firefox below 93, iOS before 16.4, plus the iOS 16.0–16.3 bug zone) who need JPEG. Drop JPEG only if your analytics show effectively zero traffic from those browsers and you can prove it.
### How do I keep CI builds fast when AVIF encoding is slow?
Three levers. Use `cavif --speed 6` or higher (defaults to 4) to trade ~5% size for ~3× speed. Parallelize across cores with GNU parallel or your build tool's worker pool. And cache by content hash so unchanged source images skip the encoder entirely. Combined, these usually cut AVIF build time below WebP's old single-threaded baseline.
### Is WebP decoding really faster than AVIF?
Yes. libwebp decodes roughly 2–3× faster than libavif, and the gap widens on low-end mobile. If your performance bottleneck is decode (a long image gallery on a budget Android phone, for example) rather than network, WebP is the better primary format. For most web traffic, network savings dominate and AVIF still wins overall.
### Can iPhone users see AVIF?
iPhones running iOS 16.4 (March 2023) or later support AVIF natively in Safari. Devices on iOS 16.0–16.3 have a documented decode bug that can crash Safari on certain AVIF files; older iOS versions don't support AVIF at all. Always ship a WebP or JPEG fallback inside `` so affected users see something.
### Should I use `` or Accept-header negotiation?
Smaller projects benefit from explicit `` markup: behavior is deterministic, you can debug locally, and it adds maybe 80 bytes of HTML per image. High-traffic sites win with CDN-side `Accept` negotiation: one URL per image, automatic format upgrades, and the CDN caches each format separately. A common hybrid is `` for app UI and CDN negotiation for marketing assets.
### Why did my PNG become larger after converting to WebP?
Almost always because the source PNG was already aggressively optimized by pngquant, oxipng, or ZopfliPNG, and the browser's Canvas-based encoder can't match those tools. Re-encode from the original (Photoshop export, design-tool master, RAW) instead of from the optimized PNG. If the optimized PNG is your only source, the original is already near-optimal; leave it alone.
### Does AVIF support transparency?
Yes. AVIF supports a full 8- to 12-bit alpha channel, and its alpha encoding is generally more compact than WebP's. A transparent PNG illustration converted to AVIF typically shrinks 50–70%, versus 30–40% as WebP. AVIF is the strongest replacement for transparent PNG in any context that doesn't demand pixel-exact lossless output.
### Can browsers encode AVIF natively via toBlob('image/avif')?
Only Chrome 99+ at the moment. Safari and Firefox can decode AVIF but cannot encode it via Canvas APIs as of May 2026. For client-side AVIF encoding you currently need WebAssembly libraries like libavif-wasm or jsquash, which add 1–2 MB of payload. Most production stacks compress to WebP in the browser and hand off AVIF generation to a server worker.
---
### What Exactly Lives Inside a PostgreSQL timestamp Column?
URL: https://go-tools.org/blog/what-is-stored-in-pg-timestamp-column
A plain-English guide to how PostgreSQL stores timestamp vs timestamptz, why timezones bite, and how to choose the right type for your use case.
# PostgreSQL timestamp vs timestamptz: What's Actually Stored Under the Hood?
PostgreSQL maintains both `timestamp` and `timestamptz` as a single 64-bit integer: the number of microseconds since 1970-01-01 00:00:00 UTC. The distinction emerges only during data formatting for human consumption.
## Why Does This Trip People Up?
- Two columns, one date... two different query results
- Your app inserts `2025-07-29 10:00`, but another team sees `02:00`
- The frontend renders an ISO string that doesn't match the backend log
## Two Cans of Peaches: One Plain, One Labeled
| Data Type | Official Name | Stored Value | What Happens on SELECT |
|-----------|---------------|--------------|------------------------|
| `timestamp` | timestamp **without** time zone | raw microsecond count | Sent back unchanged — Postgres never guesses a timezone |
| `timestamptz` | timestamp **with** time zone | same microsecond count | Postgres applies the session `TimeZone` setting just before sending the text |
### Analogy
- **`timestamp`** = a jar of peaches with no origin label. You know it's fruit, but not where it was canned.
- **`timestamptz`** = a jar proudly stamped "Made in UTC+8." Anyone opening it can decide whether to convert the nutrients panel.
## Under the Hood: It's Just a Giant Number
```
2000-01-01 00:00:00 UTC → 0
2000-01-01 00:00:01 UTC → 1 000 000
```
- **Unit**: microseconds (one-millionth of a second)
- **Range**: 4713 BC – 294276 AD — Indiana Jones approved
- Storage for `timestamp` and `timestamptz` is **identical**; interpretation differs
## A 15-Second Demo
```sql
-- Client thinks in Shanghai time
SET TimeZone = 'Asia/Shanghai';
CREATE TABLE demo (
created_ts timestamp,
created_tz timestamptz
);
INSERT INTO demo VALUES ('2025-07-29 10:00', '2025-07-29 10:00');
```
| Query | Result | Why |
|-------|--------|-----|
| `SELECT created_ts FROM demo;` | 2025-07-29 10:00:00 | Raw value, no TZ math |
| `SELECT created_tz FROM demo;` | 2025-07-29 10:00:00+08 | Tag applied on output |
| `SET TimeZone = 'UTC';` then select | 2025-07-29 02:00:00+00 | Same instant, new lens |
## Timestamp Arithmetic and Intervals
One of the most practical aspects of PostgreSQL timestamps is interval arithmetic. Because both types store microsecond counts, you can add and subtract intervals directly:
```sql
-- Add 3 hours and 30 minutes
SELECT '2025-07-29 10:00'::timestamptz + INTERVAL '3 hours 30 minutes';
-- → 2025-07-29 13:30:00+08
-- Find the difference between two timestamps
SELECT '2025-07-30 09:00'::timestamptz - '2025-07-29 10:00'::timestamptz;
-- → 23:00:00 (an interval)
-- Extract specific fields
SELECT EXTRACT(EPOCH FROM '2025-07-29 10:00:00+08'::timestamptz);
-- → 1753768800 (Unix timestamp in seconds)
-- Truncate to day boundary (useful for daily aggregations)
SELECT date_trunc('day', '2025-07-29 15:42:19+08'::timestamptz);
-- → 2025-07-29 00:00:00+08
```
The `EXTRACT(EPOCH FROM ...)` function is particularly useful when you need to pass timestamps to external systems that expect Unix epoch seconds. Conversely, you can convert an epoch back to a timestamp:
```sql
SELECT to_timestamp(1753768800);
-- → 2025-07-29 10:00:00+08 (in Asia/Shanghai session)
```
A subtle but important point: interval arithmetic with `timestamp` (without timezone) ignores DST transitions entirely, while `timestamptz` respects them. This means adding `INTERVAL '1 day'` to a `timestamptz` value that crosses a DST boundary will correctly return the same wall-clock time — not exactly 24 hours later.
## Indexing and Performance Considerations
Both `timestamp` and `timestamptz` are stored as 8-byte integers, so there is no performance difference between them for storage or indexing. B-tree indexes work identically on both types because the underlying comparison is just integer comparison.
However, there are a few practical considerations:
- **Range queries**: `WHERE created_at > '2025-07-01'` works efficiently with an index on either type. With `timestamptz`, PostgreSQL converts the literal to UTC before comparison, so the index is still used.
- **Partition keys**: When using range partitioning on timestamp columns, `timestamptz` is generally safer because partition boundaries are unambiguous (always UTC). With `timestamp`, a boundary like `'2025-07-01 00:00'` could mean different things to different sessions.
- **Functional indexes**: If you frequently query by date only (ignoring time), consider an index on `date_trunc('day', created_at)` to speed up daily aggregation queries.
## Common Pitfalls & Quick Fixes
### 1. Different users, different clocks
- **Cause**: clients use different `TimeZone` settings with `timestamptz`
- **Fix**: either keep everything `timestamp` + agree on one zone, **or** enforce `SET TimeZone = 'UTC'` at connection init
A common pattern in application code is to set the timezone once at connection pool initialization:
```sql
-- In your connection setup (e.g., pg pool config)
SET timezone = 'UTC';
```
This ensures all sessions see the same UTC representation, and your application layer handles the conversion to local time for display.
### 2. Storing "wall time" but picked the wrong type
- Business calendars (store hours, due dates) should use `timestamp`
- Cross-border workflows (orders, logs) should store **UTC** in `timestamptz`
The test is simple: if the question is "what moment in time did this happen?" use `timestamptz`. If the question is "what does the clock on the wall say?" use `timestamp`.
### 3. APIs that drift
- Always ship `timestamptz` as ISO-8601 strings with the offset (`Z` or `+08:00`)
- Let the UI format locally
### 4. Comparing timestamps across types
Mixing `timestamp` and `timestamptz` in comparisons or joins is a common source of subtle bugs:
```sql
-- Dangerous: implicit cast applies session timezone
SELECT * FROM orders o
JOIN schedules s ON o.created_tz = s.start_ts;
-- PostgreSQL casts s.start_ts to timestamptz using session timezone
-- Different sessions can get different join results!
```
**Fix**: always cast explicitly when comparing across types, or standardize on one type per domain.
### 5. ORM default pitfalls
Many ORMs (Django, SQLAlchemy, ActiveRecord) default to `timestamp` without timezone. Check your migration files — if your app serves users across timezones, override the default to `timestamptz`. In Django, set `USE_TZ = True` in settings. In SQLAlchemy, use `DateTime(timezone=True)`.
## Cheat Sheet: Which One Should I Use?
```
Local calendar only → timestamp
Anything global → timestamptz (store UTC)
```
- Financial reports, class schedules → `timestamp`
- Audit logs, e-commerce orders → `timestamptz`
## Verify in Seconds with Go Tools
| Need | Tool | How-to |
|------|------|--------|
| Inspect the epoch value from SQL | [Epoch Converter](/tools/unix-timestamp-converter) | Paste `1690622400`, hit Convert |
| Tidy bulk JSON with time fields | [JSON Formatter](/tools/json-formatter) | Drop in the payload, prettify & scan |
All utilities run entirely in your browser — no data ever leaves your machine.
## Frequently Asked Questions
### What is the difference between timestamp and timestamptz in PostgreSQL?
`timestamp` (without time zone) stores a date-time value as-is, with no timezone context. `timestamptz` (with time zone) converts the input to UTC for storage and converts back to the session's timezone on retrieval. Use `timestamptz` for almost all cases — it prevents timezone-related bugs across distributed systems.
### Does PostgreSQL actually store the timezone in timestamptz?
No — despite the name, PostgreSQL does not store the timezone itself. It converts the input to UTC and stores only the UTC value (a microsecond count from 2000-01-01). On retrieval, it converts from UTC to whatever timezone your session's `timezone` setting specifies. The original timezone information is discarded.
### How do I change the timezone for a PostgreSQL session?
Run `SET timezone = 'America/New_York';` to change the session timezone. This affects how `timestamptz` values are displayed and interpreted. For server-wide defaults, set `timezone` in `postgresql.conf`. Always use IANA timezone names (like `Asia/Shanghai`) rather than abbreviations (like `CST`) to avoid ambiguity.
### Should I use timestamp or timestamptz for storing event times?
Use `timestamptz` for nearly everything — user actions, API calls, audit logs, and scheduled events. Only use `timestamp` (without timezone) for abstract times that aren't tied to a specific moment, like "store opens at 09:00" which means 9 AM in whatever the local timezone is, not a specific UTC instant.
### How does PostgreSQL handle daylight saving time with timestamptz?
PostgreSQL handles DST correctly when using `timestamptz` because it stores everything in UTC internally. When you retrieve a value, PostgreSQL converts from UTC using the current DST rules for your session timezone. This means the same stored UTC instant correctly shows different local times before and after a DST transition.
For a comprehensive guide to Unix timestamps — including precision handling, timezone best practices, and code examples in JavaScript, Python, and Go — see our [Unix Timestamp Guide](/blog/unix-timestamp-guide-epoch-seconds-ms-timezone-dst).
## Wrap-up
- Both Postgres time types are **microsecond counters**; the label is the whole difference
- Choosing the wrong one means puzzling timestamps and broken math
- Test, convert, and sanity-check with the right tools to save hours of debugging
---
### What Is a ULID? Sortable Unique Identifier Guide
URL: https://go-tools.org/blog/what-is-ulid-sortable-unique-identifier-guide
What is a ULID? How the sortable 128-bit ID works: its timestamp-plus-randomness structure, Crockford Base32 encoding, and when to pick it over a UUID.
# What Is a ULID? The Sortable Unique Identifier, Explained
Every random UUIDv4 you insert as a primary key lands at an unpredictable spot in the database index. Do that a few million times and the index fragments, the cache thrashes, and writes slow down. A ULID fixes that without giving up what you liked about UUIDs: you can still mint one anywhere, with no central coordinator, but it lands in time order instead of scattering.
So how does a 26-character string sort itself by time? That is the whole trick, and it is worth understanding before you reach for one.
A ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier written as 26 Crockford Base32 characters. The first 10 characters encode a millisecond timestamp and the last 16 encode random bits, so ULIDs created later always sort after earlier ones when compared as plain strings. It is a **sortable unique identifier** you can generate offline.
This guide takes that apart: the anatomy decoded character by character, the proof it really sorts, the B-tree math behind the database win, and an honest look at what the embedded timestamp leaks. You can follow along with a live value in the [ULID generator](/tools/ulid-generator) — generate one, decode it, convert it to a UUID — while you read.
## What Is a ULID?
A ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier designed as a more sortable, more compact alternative to a UUID. It is written as 26 characters of Crockford Base32: the first 10 hold a 48-bit timestamp in milliseconds since the Unix epoch, and the remaining 16 hold 80 bits of randomness. Because the time comes first, the string sorts chronologically.
That last property is the reason the format exists. UUIDv4 is fully random, which is great for uniqueness but means two IDs created a second apart have no relationship to each other. ULIDs keep the coordination-free, generate-anywhere model and add time-ordering on top, so a column of them is naturally sorted by creation time with nothing extra.
Here is the format at a glance:
| Property | Value |
|----------|-------|
| Bits | 128 |
| Encoding | 26 Crockford Base32 characters |
| Layout | 48-bit timestamp + 80-bit randomness |
The rest of this article fills in how each piece works. The encoding and the sortability each get their own section below. First, the layout.
## Anatomy of a ULID: 48 Bits of Time + 80 Bits of Randomness
A ULID's 26 characters split cleanly into two halves. The first 10 characters are the timestamp; the last 16 are the random part. Lay the canonical example out and the boundary is obvious:
```
01ARYZ6S41 TSV4RRFFQ69G5FAV
└────────┘ └──────────────┘
10 chars 16 chars
48-bit ms 80-bit random
timestamp
```
Two components, two jobs. One records *when* the ULID was created; the other guarantees *uniqueness*. Each is decoded below.
### The 48-bit timestamp (first 10 characters)
The leading 10 characters encode a 48-bit integer: the number of [milliseconds since the Unix epoch](/blog/unix-timestamp-guide-epoch-seconds-ms-timezone-dst) at the moment the ULID was created. Take the canonical example straight from the spec:
```
01ARYZ6S41 -> 1469918176385 ms -> 2016-07-30T22:36:16.385Z
```
That is a real, reversible decode — paste `01ARYZ6S41TSV4RRFFQ69G5FAV` into a decoder and you get exactly `2016-07-30T22:36:16.385Z` back. The time component is plain data, not a hash, so reading it costs nothing.
One small detail that trips people up: the first character of a ULID is always between `0` and `7`. A Crockford character holds 5 bits, and 48 bits is not a multiple of 5. The timestamp occupies the low 48 of the 50 bits that 10 characters can carry, which leaves the top 2 bits of the first character permanently zero. Two zero bits cap that character's value at 7. If you ever see a ULID starting with `8` or higher, it is malformed.
### The 80 bits of randomness (last 16 characters)
The remaining 16 characters carry 80 bits of randomness, and this half is where uniqueness comes from. The bits should come from a cryptographically secure source — `crypto.getRandomValues` in the browser, not `Math.random`. The difference matters: `Math.random` is predictable enough that an attacker could guess or collide values, while a CSPRNG is not.
How much room is 80 bits? Roughly 1.2 × 10²⁴ possible values, and that is *per millisecond*. Even if you mint millions of ULIDs inside a single millisecond, the odds that two draw the same 80 bits stay vanishingly small. Unlike the timestamp, this half carries no decodable meaning — it is noise whose only purpose is to make every ULID distinct.
## Crockford's Base32: Why ULIDs Drop I, L, O, and U
ULIDs are encoded with Crockford's Base32, an alphabet of 32 symbols: the digits `0`–`9` and the letters `A`–`Z` with four removed.
```
0123456789ABCDEFGHJKMNPQRSTVWXYZ
```
The missing letters are **I, L, O, and U**. Three are dropped because they look like digits — `I` and `L` resemble `1`, `O` resembles `0` — so a human reading a ULID off a screen can't confuse a letter for a number. The flip side is forgiving input: a compliant decoder maps `I` and `L` back to `1` and `O` to `0`, and treats the whole string case-insensitively. `U` is excluded separately, to avoid accidentally spelling out offensive words.
The bit math is the other reason. Each Base32 character encodes 5 bits, where a hexadecimal character encodes only 4. Pack 128 bits at 5 bits per character and you need 26; pack the same 128 bits at 4 bits each — the way a UUID does — and you need 32, plus four hyphens, for 36 characters. So a ULID is meaningfully shorter than a UUID and, with no hyphens, drops straight into a URL, a filename, or a header without escaping.
Crockford's Base32 is an alphabet of 32 symbols (`0`–`9` and `A`–`Z` minus I, L, O, U) that encodes 5 bits per character. ULIDs use it to pack 128 bits into 26 case-insensitive, URL-safe characters, and — crucially — the alphabet is in ascending order, which is what lets the encoded string sort the same way as the raw bits.
## Why ULIDs Sort by Time
Lots of articles tell you ULIDs sort by time. Fewer show *why*. The reason rests on two facts you already have: the timestamp is the most significant part of the value, and Crockford's alphabet is laid out in ascending order.
Put those together and you get a chain of equivalences:
```
string compare == 128-bit integer compare == creation-time compare
```
Comparing two ULIDs character by character (the way a string sort works) gives the same answer as comparing their underlying 128-bit integers, *because* the alphabet is order-preserving: a "higher" character always means a higher value. Comparing the 128-bit integers gives the same answer as comparing creation times, *because* the timestamp sits in the most significant bits, so it dominates the comparison; the random tail only breaks ties within the same millisecond. String order, bit order, and time order are the same order.
A quick demonstration. Two ULIDs minted one millisecond apart:
```
01ARYZ6S41... (created at T)
01ARYZ6S42... (created at T + 1 ms)
```
The tenth character ticks from `1` to `2`, and a plain text sort puts the second after the first — no timestamp column, no special comparator. The practical payoff, which the next section expands, is one line: `ORDER BY id` returns rows in chronological order with no extra index.
## ULIDs as Database Primary Keys: B-Tree Locality
Most relational databases store a primary-key index as a B-tree, and where a new key lands in that tree decides how expensive the insert is. This is where ULIDs earn their keep.
A random UUIDv4 lands somewhere unpredictable on every insert:
> **UUIDv4:** each new key targets a random leaf page. The page is often full, so the engine splits it, copies half the rows elsewhere, and dirties pages all over the tree. Across millions of rows this fragments the index, evicts useful pages from the buffer cache, and drags down insert throughput. (For the hard [index page-split numbers](/blog/uuid-v4-v7-ulid-snowflake-id-comparison) — typically a 2–10× difference on write-heavy tables — see the comparison guide.)
A time-prefixed ULID lands at the end every time:
> **ULID:** because the high bits are a timestamp, each new key is greater than the last, so it appends at or near the right edge of the index. Inserts stay sequential, page splits nearly disappear, the index stays compact, and a range scan over a time window reads a contiguous run of pages.
You get the coordination-free generation of a UUID with the insert locality of an auto-increment integer — without exposing a guessable sequential counter, since the random tail still hides the exact next value.
**Storage tip:** store the 128 bits as 16 binary bytes — a `uuid` column in PostgreSQL, `BINARY(16)` in MySQL — not as a 26-character text field, which wastes space and bloats the index. Encode to the Base32 string only at the edges where a human or a URL sees it. The generator's Convert tab will [convert a ULID to a UUID](/tools/ulid-generator) for exactly this, since the two forms are the same 128 bits.
## Monotonic ULIDs: Strict Order Within a Millisecond
The sortability proof has one honest gap: within a single millisecond, plain ULIDs are *not* strictly ordered. They share the same 10-character time prefix, but their 80-bit random tails are drawn independently, so which of two same-millisecond ULIDs sorts first is essentially a coin flip. For most uses that is fine. When you need strict order even at sub-millisecond rates, it is not.
Monotonic generation closes the gap. The rule is simple: the first ULID in a given millisecond gets fresh randomness as usual, and every later ULID in that same millisecond is produced by taking the previous 80-bit random value and incrementing it by one (treated as a big-endian integer, carrying into higher bits as needed). Each value is therefore strictly greater than the one before it.
You can see it in a batch generated inside one millisecond — only the final character moves:
```
01KVT0F720ZK9N4T2QX7VR8WMC
01KVT0F720ZK9N4T2QX7VR8WMD
01KVT0F720ZK9N4T2QX7VR8WME
```
`…WMC` < `…WMD` < `…WME`, guaranteed. This matters whenever rows can be created faster than the millisecond clock ticks: high-throughput inserts, event logs, message IDs in a tight loop. When the clock advances to the next millisecond, generation reverts to fresh randomness and the cycle repeats.
## ULID vs UUID: When to Use Which
The question most people actually arrive with is **ULID vs UUID**. Here is the focused comparison — ULID against the two UUID versions you'd realistically weigh it against. (For the full five-way decision matrix including Snowflake and NanoID, see the [full comparison of ULID, UUID and Snowflake](/blog/uuid-v4-v7-ulid-snowflake-id-comparison).)
| Property | ULID | UUIDv4 | UUIDv7 |
|----------|------|--------|--------|
| Length | 26 chars | 36 chars | 36 chars |
| Encoding | Crockford Base32 | Hyphenated hex | Hyphenated hex |
| Sortable by time? | Yes | No | Yes |
| Embeds timestamp? | Yes (48-bit ms) | No | Yes (48-bit ms) |
| Standardized? | Community spec | RFC 9562 | RFC 9562 |
| Best for | Short sortable IDs | Opaque random IDs | Sortable IDs in UUID format |
In prose: reach for a **ULID** when you want the shortest, URL-safe, sortable string. Reach for **UUIDv4** when you want an opaque, fully random identifier with no embedded time — for example a public token where you'd rather not reveal when it was created. Reach for [UUIDv7](/tools/uuid-generator) when you need time-ordering but must stay inside the standard UUID format, with version and variant bits in their fixed positions and a native `uuid` column to drop it into.
All three are 128 bits, so ULID ↔ UUID conversion is lossless either way. The relationship between ULID and **ulid vs uuid v7** is closer than it looks: UUIDv7 is essentially the IETF-standardized take on the same time-prefixed idea ULID pioneered. If you're [new to UUIDs](/blog/what-is-uuid-guide-format-versions-use-cases) altogether, start with the fundamentals first, then come back to this comparison.
## The Privacy Trade-Off: ULIDs Leak Their Creation Time
The embedded timestamp is a feature and a leak, depending on who reads the ID. Anyone holding a ULID can [decode the timestamp](/tools/ulid-generator) in one step and learn the exact millisecond the record was created — no access to your database required.
Inside your own systems that is pure upside: instant auditing, free ordering, easy debugging. On a *public-facing* identifier it is a real disclosure. The creation time can be business-sensitive on its own, and a handful of ULIDs sampled over time leak your creation *rate*: how many orders, accounts, or messages you mint per second. That is the kind of thing competitors and scrapers like to estimate.
To be fair, this is a narrower leak than UUIDv1, which historically embedded the generating machine's MAC address; a ULID exposes only time, never hardware identity. Still, weigh it. The simple mitigation: keep ULIDs internal and hand out a fully random UUIDv4 for public-facing IDs where ordering doesn't matter.
## Common Pitfalls with ULIDs
Most ULID trouble is a handful of avoidable engineering decisions, not bugs in the format. The recurring ones:
- **Assuming same-millisecond plain ULIDs are ordered.** They share a time prefix but have independent random tails, so their order is undefined. *Fix:* use monotonic mode when you need strict ordering at sub-millisecond rates.
- **Storing a ULID as 26-char text.** That wastes space and inflates the index. *Fix:* store the 128 bits as 16 bytes (`uuid` / `BINARY(16)`) and encode to Base32 only at the edges.
- **Expecting a ULID→UUID conversion to report as v4 or v7.** Conversion re-encodes the same bits; it does not set the UUID version and variant fields, so a library inspecting them won't see a tagged version. *Fix:* treat the result as an opaque 128-bit value, or generate a real UUIDv7 when you need the tag.
- **Filling the randomness with `Math.random`.** It is predictable and can collide. *Fix:* always use a CSPRNG like `crypto.getRandomValues`.
- **Exposing ULIDs publicly without weighing the timestamp leak.** See the privacy section above. *Fix:* internal ULIDs, random UUIDv4 for public IDs.
- **Hand-typing `I`, `L`, `O`, or `U` into a ULID.** Those letters aren't in the alphabet, and retyping invites errors. *Fix:* copy ULIDs, don't retype them.
## FAQ
### Is ULID an official standard like UUID?
No. ULID is a community specification published on GitHub, not an IETF RFC. It is widely implemented and stable, but it has no standards body behind it. If you need a standardized, time-ordered identifier, UUIDv7 (RFC 9562) applies the same idea inside the official UUID format.
### How many characters is a ULID, and why is it shorter than a UUID?
26 characters, versus a UUID's 36. ULID uses Crockford Base32, which packs 5 bits per character; a UUID's hexadecimal packs only 4 bits and adds four hyphens. The same 128 bits therefore need fewer characters in Base32 — and none of them need URL escaping.
### Can two ULIDs ever collide?
Practically never. Within one millisecond a ULID has 80 random bits — about 1.2 × 10²⁴ possibilities — so even generating millions per millisecond keeps the collision odds vanishingly small. The one requirement is that a cryptographically secure RNG fills the randomness; `Math.random` voids the guarantee.
### Can I store ULIDs in PostgreSQL or MySQL?
Yes. A ULID is 128 bits, so convert it to UUID form and store it in a `uuid` column (PostgreSQL) or `BINARY(16)` (MySQL), then render the Base32 string only at the edges. There is no native ULID column type, but the UUID representation costs the same 16 bytes and keeps the index compact.
### Are ULIDs case-sensitive?
The canonical form is uppercase, but Crockford Base32 is case-insensitive on input: a decoder reads lowercase letters the same way, and maps `I`/`L` to `1` and `O` to `0`. To avoid surprises in equality checks and indexes, normalize to a single case before you store or compare.
### Will the 48-bit timestamp ever run out?
Not for a very long time. 48 bits of milliseconds reach the year 10889 before the counter overflows, so the timestamp component is effectively future-proof for any real application. You will replace the system, the language, and the database long before the format runs out of room.
### Can I generate ULIDs in the browser or on mobile without a server?
Yes — that's a core benefit. ULIDs need no central coordinator, so any node, edge worker, browser, or device can mint one from its clock plus a secure RNG. Values created on different machines still sort together by time afterward, because the timestamp lives in the ID itself.
## Conclusion
ULIDs solve a specific, real problem — random keys fragmenting your index — without taking away decentralized generation. The mechanics are worth keeping in mind:
- A ULID is a **48-bit millisecond timestamp + 80 bits of randomness**, encoded as 26 Crockford Base32 characters.
- It sorts by time because the timestamp is the most significant component and the alphabet is order-preserving — string order equals time order.
- That ordering gives a B-tree the insert locality a random UUIDv4 lacks, keeping writes fast and the index compact.
- Use monotonic mode when you need strict ordering for IDs minted in the same millisecond.
- Weigh the timestamp leak before exposing ULIDs on public-facing identifiers.
- Pick UUIDv7 instead when you must stay inside the standard UUID format.
When you're ready to put it to work, open the [ULID generator](/tools/ulid-generator) to generate, decode, and convert ULIDs entirely in your browser — no server, no upload, nothing stored.
---
### What Is a UUID? Guide to Format, Versions & Use Cases
URL: https://go-tools.org/blog/what-is-uuid-guide-format-versions-use-cases
UUIDs from the ground up: 128-bit structure, hex format, how v1/v3/v4/v5/v7 work internally, collision math, real-world use cases and code examples.
# UUID Explained: 128-Bit Structure, Versions & Real-World Use Cases
Every time you sign up for a service, a unique identifier is created for your account. Every API request carries a trace ID. Every row in a distributed database needs a primary key that won't collide with keys generated on other machines. The solution behind all of these? **UUID — Universally Unique Identifier.**
This guide explains what UUIDs are, how they're structured, what each version does under the hood, and when to use (or avoid) them.
## UUID at a Glance
A UUID is a **128-bit (16-byte) identifier** designed to be globally unique without requiring a central authority. It's written as 32 hexadecimal digits in the canonical **8-4-4-4-12** format:
```
550e8400-e29b-41d4-a716-446655440000
|------| |--| |--| |--| |----------|
8 hex 4 4 4 12 hex
```
That's 32 hex characters + 4 hyphens = 36 characters total. The hyphens are purely cosmetic — they don't carry data.
**Key facts:**
- **128 bits** = 2¹²⁸ ≈ 3.4 × 10³⁸ possible values
- Standardized by **RFC 9562** (May 2024, supersedes RFC 4122)
- Also called **GUID** (Globally Unique Identifier) in Microsoft ecosystems — same format, different name
- Supported natively by PostgreSQL (`uuid` type), MySQL (`BINARY(16)` or `CHAR(36)`), and virtually every programming language
## Anatomy of a UUID
Every UUID encodes two metadata fields in fixed bit positions, regardless of version:
```
550e8400-e29b-41d4-a716-446655440000
^ ^
| |
Version-┘ └-Variant
```
### Version Field (Bits 48–51)
The 13th hex digit (first digit of the third group) identifies the UUID version:
| Hex Digit | Version | Method |
|---|---|---|
| `1` | v1 | Timestamp + MAC address |
| `3` | v3 | MD5 hash of namespace + name |
| `4` | v4 | Cryptographically random |
| `5` | v5 | SHA-1 hash of namespace + name |
| `6` | v6 | Reordered timestamp (RFC 9562) |
| `7` | v7 | Unix timestamp + random (RFC 9562) |
| `8` | v8 | Custom / implementation-specific |
### Variant Field (Bits 64–65)
The 17th hex digit (first digit of the fourth group) identifies the variant. For RFC 4122/9562 UUIDs, the first bits are `10`, which means this hex digit is always `8`, `9`, `a`, or `b`.
### Example Breakdown
```
550e8400-e29b-41d4-a716-446655440000
↑ ↑
4 → v4 a → RFC 4122 variant
This is a UUID v4 (random), RFC 4122/9562 variant.
```
## UUID Versions Explained
### Version 1: Timestamp + MAC Address
UUID v1 was the original design. It encodes:
- **60-bit timestamp** — 100-nanosecond intervals since October 15, 1582 (the Gregorian calendar reform)
- **14-bit clock sequence** — monotonicity counter to prevent duplicates on clock rollback
- **48-bit node** — typically the machine's MAC address
```
| Timestamp | Ver | Clk |Var| Node (MAC) |
| 60 bits | 4b | 14b |2b | 48 bits |
```
**Problems:**
- Exposes the generation time and hardware identity (privacy risk)
- MAC addresses can be spoofed, undermining uniqueness
- The 1582 epoch is confusing and requires conversion
**Verdict:** Deprecated by RFC 9562. Use v7 instead for time-based UUIDs.
### Version 3: MD5 Name-Based (Deterministic)
UUID v3 hashes a **namespace UUID** and a **name string** using MD5. The same inputs always produce the same UUID.
```python
import uuid
# namespace = DNS, name = "example.com"
print(uuid.uuid3(uuid.NAMESPACE_DNS, "example.com"))
# → "9073926b-929f-31c2-abc9-fad77ae3e8eb" (always this value)
```
Four standard namespaces are defined:
- **DNS**: `6ba7b810-9dad-11d1-80b4-00c04fd430c8`
- **URL**: `6ba7b811-9dad-11d1-80b4-00c04fd430c8`
- **OID**: `6ba7b812-9dad-11d1-80b4-00c04fd430c8`
- **X.500**: `6ba7b814-9dad-11d1-80b4-00c04fd430c8`
**Verdict:** Functional but prefer v5 — SHA-1 is stronger than MD5.
### Version 4: Random — The Most Popular
UUID v4 fills **122 bits** with cryptographically secure random data (the remaining 6 bits are reserved for the version and variant fields).
```
| Random | Ver | Random |Var| Random |
| 48 bits | 4b | 12 bits |2b | 62 bits |
```
With 2¹²² ≈ 5.3 × 10³⁶ possible values, the probability of collision is astronomically low. To reach a 50% chance of at least one collision, you'd need approximately **2.71 × 10¹⁸ UUIDs** — that's 2.71 quintillion.
```javascript
// Every modern browser and Node.js supports this
const id = crypto.randomUUID();
console.log(id); // → "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
```
**Strengths:** simple, private, universally supported, no coordination needed.
**Weakness:** random distribution causes B-tree index fragmentation when used as database primary keys. For database-heavy use cases, consider v7.
### Version 5: SHA-1 Name-Based (Deterministic)
Identical to v3 but uses SHA-1 instead of MD5. Same inputs always produce the same UUID.
```python
import uuid
print(uuid.uuid5(uuid.NAMESPACE_DNS, "example.com"))
# → "cfbff0d1-9375-5685-968c-48ce8b15ae17" (always this value)
```
**Use cases:**
- Generating stable IDs from URLs or DNS names
- Content-addressable storage keys
- Reproducible test fixtures
**Important:** v3 and v5 are NOT meant for security. They are deterministic — anyone who knows the namespace and name can reproduce the UUID.
### Version 7: Unix Timestamp + Random (Recommended for New Projects)
UUID v7 is the newest version, introduced in **RFC 9562 (May 2024)**. It encodes:
- **48-bit Unix timestamp** in milliseconds — monotonically increasing
- **74 bits** of cryptographic randomness
```
| Unix timestamp (ms) | Ver | rand_a |Var| rand_b |
| 48 bits | 4b | 12 bits |2b | 62 bits |
```
This means v7 UUIDs are **naturally sorted by creation time** — newer UUIDs are always lexicographically greater than older ones. This property makes them ideal for database primary keys, where B-tree indexes stay sequential rather than fragmenting randomly.
```javascript
import { v7 as uuidv7 } from "uuid";
const id1 = uuidv7(); // generated at T₁
const id2 = uuidv7(); // generated at T₂ (T₂ > T₁)
console.log(id1 < id2); // → true (lexicographic comparison)
```
**Why it matters for databases:** v7's sequential property reduces index page splits by up to 90% compared to v4, resulting in faster inserts, smaller indexes, and better cache performance.
## UUID vs GUID — What's the Difference?
There is no functional difference. **GUID** (Globally Unique Identifier) is Microsoft's name for UUID, used in Windows, .NET, COM, and SQL Server. The format is identical: 128 bits, 8-4-4-4-12 hex.
The only cosmetic difference: Microsoft tools sometimes display GUIDs in **uppercase with curly braces**:
```
UUID: 550e8400-e29b-41d4-a716-446655440000
GUID: {550E8400-E29B-41D4-A716-446655440000}
```
If someone asks about the "difference between UUID and GUID," the answer is: branding.
## Special UUID Values
RFC 9562 defines two special UUIDs:
| Name | Value | Purpose |
|---|---|---|
| **Nil UUID** | `00000000-0000-0000-0000-000000000000` | Represents absence of value (like `null`) |
| **Max UUID** | `ffffffff-ffff-ffff-ffff-ffffffffffff` | Boundary marker or sentinel value |
Never use these as actual identifiers — they are not unique by definition.
## Collision Probability: The Birthday Problem
The "birthday problem" calculates how many UUIDs you need before a collision becomes likely. For UUID v4 (122 random bits):
| UUIDs Generated | Collision Probability |
|---|---|
| 1 million | ~10⁻²² (virtually impossible) |
| 1 billion | ~10⁻¹⁶ (still negligible) |
| 2.71 × 10¹⁸ | 50% (the "birthday bound") |
To put it in context: if you generated **1 billion UUIDs per second**, it would take **86 years** to reach a 50% chance of a single collision. In practice, hardware failure, software bugs, and cosmic rays are all more likely to cause a duplicate than UUID v4 math.
The formula: p(n) ≈ n² / (2 × 2¹²²)
## How to Validate a UUID
A valid UUID matches this regex pattern (case-insensitive):
```
^[0-9a-f]{8}-[0-9a-f]{4}-[1-7][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
```
This checks:
1. The 8-4-4-4-12 hex format
2. Version digit is 1–7 (position 15)
3. Variant nibble starts with 8, 9, a, or b (position 20)
```javascript
function isValidUUID(str) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-7][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(str);
}
isValidUUID("550e8400-e29b-41d4-a716-446655440000"); // → true
isValidUUID("not-a-uuid"); // → false
```
## Generating UUIDs in Every Language
### JavaScript / TypeScript
```javascript
// Browser & Node.js — built-in v4
crypto.randomUUID();
// npm uuid package — supports v1, v3, v4, v5, v7
import { v4, v7 } from "uuid";
v4(); // random
v7(); // time-ordered
```
### Python
```python
import uuid
uuid.uuid4() # random
uuid.uuid5(uuid.NAMESPACE_DNS, "example.com") # deterministic
# uuid.uuid7() planned for Python 3.14+
```
### Go
```go
import "github.com/google/uuid"
uuid.New() // v4 random
uuid.Must(uuid.NewV7()) // v7 time-ordered
```
### Java
```java
import java.util.UUID;
UUID.randomUUID(); // v4 random
// UUID v7: use com.fasterxml.uuid or java.util.UUID in JDK 21+
```
### SQL (PostgreSQL)
```sql
-- v4 (PostgreSQL 13+)
SELECT gen_random_uuid();
-- v7 (PostgreSQL 18+)
SELECT uuidv7();
```
## Common Use Cases
### Database Primary Keys
UUIDs let you generate IDs anywhere — in the application, on the client, at the edge — without a database round trip. This enables offline-first architectures and simplifies distributed systems. Use **v7** for best index performance, or **v4** if you don't care about ordering.
### API Request Tracing
Assign a UUID to every API request at the entry point (gateway, load balancer). Pass it through all downstream services in a header like `X-Request-ID`. This makes it trivial to correlate logs across microservices.
### Idempotency Keys
APIs use UUIDs as idempotency keys to ensure that retried requests don't create duplicate resources. The client generates a UUID before the first attempt and sends the same UUID on retries.
### Session Identifiers
UUIDs provide sufficient uniqueness to prevent session collisions across large user bases. Unlike auto-increment integers, they can't be enumerated — an attacker can't guess valid session IDs by incrementing a number.
### Content-Addressable Storage
UUID v5 generates deterministic IDs from content. Given the same input, you always get the same UUID — useful for deduplication, caching, and reproducible builds.
## Security Considerations
### UUIDs Are NOT Security Tokens
UUIDs are designed for **uniqueness**, not **secrecy**. Key issues:
- **UUID v1** leaks the generation timestamp and MAC address
- **UUID v4** has 122 random bits but a predictable structure (version/variant bits are fixed)
- **UUID v3/v5** are deterministic — anyone who knows the namespace and name can reproduce the UUID
For security tokens, API keys, or session secrets, use a dedicated CSPRNG with 128+ bits of pure randomness:
```javascript
// For security tokens — NOT a UUID, but fully random
const token = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
```
### UUID v7 Exposes Creation Time
The first 48 bits of a UUID v7 encode the creation timestamp in milliseconds. Anyone who receives a v7 UUID can extract when it was created:
```javascript
const hex = "01906b5e-4a3e-7234-8f56-b8c12d4e5678".replace(/-/g, "").slice(0, 12);
new Date(parseInt(hex, 16));
// → 2024-07-01T12:34:56.000Z
```
If creation time is sensitive information, use v4 instead.
### Don't Use UUIDs to Prevent Enumeration
While UUIDs are harder to guess than sequential integers, they shouldn't be your only access control mechanism. Always enforce authorization checks — don't rely on URL obscurity.
## Frequently Asked Questions
### Why are there hyphens in UUIDs?
The hyphens in the 8-4-4-4-12 format are purely for human readability. They carry no data and are ignored during parsing. Some systems store UUIDs without hyphens (32 hex characters), which is equally valid.
### Can two UUIDs ever be the same?
Theoretically yes, practically no. For UUID v4 with 122 random bits, the probability of generating two identical UUIDs is approximately 1 in 5.3 × 10³⁶ for any given pair. At real-world generation rates, you are more likely to be struck by lightning while winning the lottery than to encounter a UUID collision.
### Are UUIDs sequential?
Only some versions. UUID v1, v6, and v7 contain timestamps and sort chronologically. UUID v4 is fully random with no ordering. UUID v3 and v5 are deterministic but not ordered.
### How much storage does a UUID use?
- **Binary**: 16 bytes (128 bits) — the most efficient storage
- **String (with hyphens)**: 36 bytes (ASCII)
- **String (without hyphens)**: 32 bytes (ASCII)
Most databases store UUIDs in binary format internally. PostgreSQL's native `uuid` type uses exactly 16 bytes.
### Should I use UUID or auto-increment for primary keys?
Auto-increment is simpler for single-database applications (smaller, faster, sequential). UUID is better for distributed systems (generate anywhere, no coordination, merge-safe). If using UUID, prefer v7 for best database performance.
### What is RFC 9562?
RFC 9562, published in May 2024, is the latest UUID standard. It supersedes RFC 4122 and formally introduces UUID versions 6, 7, and 8. It deprecates v1 in favor of v6/v7 and defines the nil and max UUID values. If you're implementing UUID generation or validation, RFC 9562 is the authoritative reference.
### Can I use UUIDs across different programming languages?
Yes. The UUID format (128-bit, 8-4-4-4-12 hex) is language-agnostic. A UUID generated in JavaScript will be correctly parsed in Python, Go, Java, or any other language with UUID support. This interoperability is one of UUID's greatest strengths.
---
*Generate, decode, and validate UUIDs instantly with our [UUID Generator](/tools/uuid-generator) — supports v1, v4, v5, and v7 with batch generation, 100% in your browser.*
*Choosing between UUID versions for your next project? Read our [UUID v4 vs v7 vs ULID vs Snowflake comparison](/blog/uuid-v4-v7-ulid-snowflake-id-comparison) for a practical selection guide with database benchmarks and code examples.*
---
### XML to JSON: Conventions, Pitfalls & Code (2026 Guide)
URL: https://go-tools.org/blog/xml-to-json-conversion-guide
Convert XML to JSON the right way: how attributes, arrays, and namespaces map, why values stay strings, plus code for JavaScript, Python and the browser.
# XML to JSON Conversion: Conventions, Pitfalls & Code Examples
You pull a response off a SOAP endpoint, an RSS feed, or a `sitemap.xml`, and it's XML. Your stack is JSON-native: JavaScript on the front end, REST in the middle, a document store at the bottom. So you need to convert XML to JSON, and you reach for a parser expecting it to be a one-liner.
It usually is — until the output bites you. An array you expected turns out to be a single object. An `id` attribute vanishes. A ZIP code like `01234` comes back as the number `1234`. None of these are bugs in your parser. They're the consequence of mapping two data models that don't line up, and the only way to convert XML to JSON reliably is to understand the conventions that bridge the gap.
This guide covers why those conventions exist, four ways to do the conversion (browser, JavaScript, Python, CLI), the `@_` and `#text` rules every major library shares, the five pitfalls that cause silent data loss, and how to convert JSON back to XML for a clean round-trip. Paste the examples into Node, Python, or a shell and they produce the output shown in the comments.
## Why XML-to-JSON Needs Conventions (Not Just a Reformat)
XML and JSON look similar at a glance: both are trees of named, nested data. But their underlying models diverge. XML elements can carry attributes, hold mixed content (text interleaved with child elements), and live under namespaces. JSON has none of those concepts. It has objects, arrays, and four scalar types. Converting one to the other isn't reformatting; it's translating between two grammars, and one of them has words the other can't spell.
Before you convert anything, it pays to confirm the source is actually valid. A stray unescaped `&` or a mismatched tag will reject at the parser, so running the input through an [XML Formatter](/tools/xml-formatter) to check well-formedness first saves a round of confusing errors.
Here is where the two models pull apart:
| Dimension | XML | JSON |
|-----------|-----|------|
| Node types | elements, attributes, text, mixed content | objects, arrays, string, number, boolean, null |
| Root constraint | exactly one root element required | no root constraint |
| Attributes | yes (`id="P01"`) | none (needs an `@_` convention) |
| Repeated elements | same-named siblings are legal | object keys can't repeat (needs an array convention) |
| Type system | text is untyped — everything is a string | native types |
| Namespaces | yes (`xmlns`) | none |
Because the models don't match, every XML-to-JSON conversion is convention-driven, not lossless reformatting. The conventions aren't arbitrary, though: `fast-xml-parser` (Node.js), `xmltodict` (Python), and JAXB (Java) all landed on the same two markers, `@_` for attributes and `#text` for mixed-content text. Learn them once and they transfer across runtimes. Data-shape mismatches like this show up in other conversions too, such as the type-inference questions in the [CSV to JSON conversion guide](/blog/csv-json-conversion-guide).
## How to Convert XML to JSON: 4 Methods
Pick the method that fits your context: a quick one-off paste, a Node service, a Python pipeline, or a shell script in CI.
### Method 1 — Browser-Based Tool (Zero Setup, Privacy-First)
For a one-off conversion, or for XML you'd rather not paste into a random website, an in-browser converter is the fastest path. Paste XML into the [XML to JSON Converter](/tools/xml-to-json), and the JSON appears instantly — no install, no account, no upload. Everything runs in your browser's JavaScript engine, so the data never leaves the machine.
That detail matters here. SOAP envelopes carry WS-Security tokens, internal configs carry connection strings, and exports carry customer records. Because nothing is transmitted, the tool is safe for XML containing credentials or sensitive payloads. You can confirm it yourself: open the Network tab and watch zero requests fire as you convert.
### Method 2 — JavaScript / Node.js (fast-xml-parser)
In Node, `fast-xml-parser` is the standard choice. The defaults will surprise you, though — attributes are ignored and values get coerced — so the options below are the ones you actually want for a faithful conversion:
```javascript
// Convert XML to JSON in Node.js using fast-xml-parser
import { XMLParser } from 'fast-xml-parser';
const xml = `
Wireless Headphones
79.99
`;
const parser = new XMLParser({
ignoreAttributes: false, // keep attributes (default drops them!)
attributeNamePrefix: '@_', // attributes become @_-prefixed keys
textNodeName: '#text', // mixed-content text goes under #text
parseAttributeValue: false, // no type coercion on attributes
parseTagValue: false, // no type coercion on element text
});
const result = parser.parse(xml);
console.log(JSON.stringify(result, null, 2));
// {
// "catalog": {
// "product": {
// "@_id": "P01",
// "name": "Wireless Headphones",
// "price": {
// "@_currency": "USD",
// "#text": "79.99"
// }
// }
// }
// }
```
The two settings people forget are `ignoreAttributes: false` and `parseTagValue: false`. The first keeps your `id` and `currency` attributes; the second stops the parser from turning `"79.99"` into a float and `"01234"` into `1234`. We'll come back to why string preservation is the safe default in the pitfalls section.
If you want zero dependencies in the browser, the native `DOMParser` does the parsing for you, and you walk the DOM yourself:
```javascript
// Zero-dependency XML to JSON in the browser using DOMParser
function xmlToJson(node) {
// Text-only element → string value
const children = Array.from(node.children);
if (children.length === 0 && node.attributes.length === 0) {
return node.textContent.trim();
}
const obj = {};
// Attributes → @_ prefix
for (const attr of node.attributes) {
obj['@_' + attr.name] = attr.value;
}
// Element with attributes AND text → #text
if (children.length === 0) {
obj['#text'] = node.textContent.trim();
return obj;
}
// Recurse into children, collecting same-named siblings into arrays
for (const child of children) {
const value = xmlToJson(child);
if (obj[child.tagName] === undefined) {
obj[child.tagName] = value;
} else {
if (!Array.isArray(obj[child.tagName])) obj[child.tagName] = [obj[child.tagName]];
obj[child.tagName].push(value);
}
}
return obj;
}
const doc = new DOMParser().parseFromString(
'Wireless Headphones ',
'text/xml'
);
const json = { [doc.documentElement.tagName]: xmlToJson(doc.documentElement) };
console.log(JSON.stringify(json, null, 2));
// { "catalog": { "product": { "@_id": "P01", "name": "Wireless Headphones" } } }
```
`DOMParser` is XML 1.0 compliant, handles CDATA and entity references, and reports well-formedness errors — all without a package install. The trade-off is that you own the traversal logic, including the array-collection rule shown above.
### Method 3 — Python (xmltodict)
In Python, `xmltodict` collapses the whole job into a short pipeline. It uses `@` as its attribute prefix and `#text` for mixed content by default:
```python
# Convert XML to JSON in Python using xmltodict
import json
import xmltodict
xml = """
Wireless Headphones
79.99
"""
data = xmltodict.parse(xml)
print(json.dumps(data, indent=2))
# {
# "catalog": {
# "product": {
# "@id": "P01",
# "name": "Wireless Headphones",
# "price": {
# "@currency": "USD",
# "#text": "79.99"
# }
# }
# }
# }
```
By default `xmltodict` keeps every value as a string, which is the behavior you want. The one option worth knowing up front is `force_list`, which fixes the single-versus-many array problem before it reaches your code:
```python
# force_list guarantees is always a list, even when there is one
data = xmltodict.parse(xml, force_list={'product'})
products = data['catalog']['product'] # always a list now
for p in products:
print(p['name'])
```
Without `force_list`, one `` yields a dict and two yield a list — and your loop crashes on the single-item case. That's pitfall #1, which we cover below.
### Method 4 — CLI (yq / Python one-liner)
For shell scripts and CI pipelines, two one-liners cover most cases. Mike Farah's `yq` reads XML and emits JSON directly:
```bash
# Using yq (Mike Farah's Go version)
yq -p=xml -o=json '.' input.xml
# Pipe from stdin
cat sitemap.xml | yq -p=xml -o=json '.'
```
If `xmltodict` is already in your environment, the Python one-liner needs no extra binary:
```bash
python3 -c "import sys, xmltodict, json; print(json.dumps(xmltodict.parse(sys.stdin.read()), indent=2))" < input.xml
```
Both stream from stdin, so they drop straight into a pipeline — useful for converting an API response mid-script or normalizing a batch of files in a build step.
## The @_ Attribute and #text Conventions Explained
Most converter pages skip the part that actually matters: what the odd-looking `@_` and `#text` keys mean and why they exist. Once these click, the output stops looking arbitrary.
**Attributes map to `@_`-prefixed keys.** An attribute has no JSON equivalent — there's no slot in an object for "metadata about this object" that's distinct from a child. The convention is to give attributes a key prefixed with `@_`:
```
→ { "user": { "@_id": "42", "@_role": "admin" } }
```
Why `@_` specifically? Because no valid XML element name can start with `@`, the prefix can never collide with a real child-element key. The character is reserved for free. (`xmltodict` uses bare `@`; `fast-xml-parser` uses `@_` by default. The principle is identical.)
**Mixed content maps to `#text`.** When an element has both an attribute and a text value, the text needs somewhere to live alongside the attribute keys. That's `#text`:
```
29.99
→ { "price": { "@_currency": "USD", "#text": "29.99" } }
```
**Plain-text elements become a direct string value.** No attributes, no children, just text — so there's no need for the `#text` indirection. `Alice ` becomes `"name": "Alice"`. The `#text` key only appears when attributes force the element value to be an object.
This asymmetry is the source of a subtle bug. The same element name can produce a plain string in one document and an `@_`/`#text` object in another, depending on whether that particular instance carried an attribute. A `` with no `currency` attribute is the string `"29.99"`; the same `` is `{ "@_currency": "USD", "#text": "29.99" }`. Code that reads `node.price` directly works for one shape and silently breaks on the other. The defensive accessor is to check the type: `const amount = typeof node.price === 'object' ? node.price['#text'] : node.price;`.
**CDATA becomes plain text content.** A `` section is just an escaping mechanism, so the delimiters are stripped and the inner text is preserved: `"if (a < b) return;"`. Nothing special survives into the JSON.
Once you have output, paste it into a [JSON Formatter](/tools/json-formatter) to validate the JSON output and confirm the structure matches what your consumer expects before you wire it into code.
## 5 XML-to-JSON Pitfalls & How to Avoid Them
These are the failures that get past code review and show up in production. Each one traces back to the model mismatch from the start of this guide.
**1. Array ambiguity (one vs. many).** A single `- ` becomes an object; two or more become an array. The JSON shape depends on how many siblings happened to be in that particular document. Consumer code like `result.items.item.forEach(...)` works in testing — where your fixture has three items — and throws `TypeError: not a function` in production when a record has exactly one.
```javascript
// Two
siblings → array
// A B
// → { "library": { "book": ["A", "B"] } }
// One → object, NOT an array
// A
// → { "library": { "book": "A" } }
// Normalize so both cases behave identically
const books = [].concat(result.library?.book ?? []);
books.forEach(b => console.log(b)); // safe for 0, 1, or many
```
The `[].concat(x ?? [])` idiom is worth memorizing: a missing value becomes `[]`, a single object becomes `[object]`, and an existing array passes through unchanged. In Python, pass `force_list={'book'}` to `xmltodict.parse()` and the value is always a list, so you skip the normalization entirely.
**2. Attributes silently dropped.** Several libraries default to ignoring attributes — `fast-xml-parser` does exactly this until you set `ignoreAttributes: false`. The conversion looks like it worked, the JSON parses fine, and your `id`, `currency`, and `status` values are simply gone. Always set the flag explicitly rather than trusting the default.
**3. Namespace flattening.** An `xmlns` declaration becomes an ordinary `@_xmlns` key, and the prefix in `` survives only as part of the string key `"soap:Body"`. The *semantics* — that two prefixes might bind to the same URI — are lost.
```
...
→ {
"soap:Envelope": {
"@_xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
"soap:Body": "..."
}
}
```
The prefix `soap:` is now just text in a key name; nothing knows it's a namespace. If two elements from different namespaces share a local name, they can collide. When precise namespace handling is part of the requirement, keep the data in a namespace-aware parser and don't flatten it into JSON at all.
**4. No type coercion — and that's correct.** `01234 ` must not become `1234`. Account codes, postal codes, padded identifiers, and precision-sensitive decimals all break under silent coercion. A good converter keeps everything as a string and lets you coerce deliberately:
```javascript
// Don't rely on implicit coercion
if (config.timeout > 25) { /* fragile: "30" > 25 happens to work */ }
// Coerce explicitly, only where you know the type
if (parseInt(config.timeout, 10) > 25) { /* safe */ }
```
**5. Lossy: comments, processing instructions, and mixed-content order.** XML comments (``) and processing instructions (``) have no JSON home and are discarded. The relative order of text interleaved with child elements may not round-trip. If you need every byte preserved — for re-emitting the exact source document — don't convert at all; use an [XML Formatter](/tools/xml-formatter) to reformat or minify without touching the data model.
## Converting JSON Back to XML (Round-Trip)
Going the other direction has its own twist, because JSON has no root-element rule and XML requires exactly one. The companion [JSON to XML Converter](/tools/json-to-xml) applies the same `@_`/`#text` conventions in reverse, so a JSON → XML → JSON trip preserves attributes, text, and structure.
The interesting part is root normalization. The converter resolves the single-root requirement with four rules:
- **Single-key object** → that key becomes the root: `{ "config": {...} }` → `... `.
- **Multi-key object** → wrapped in ``: `{ "a": 1, "b": 2 }` → `1 2 `.
- **Top-level array** → wrapped as `- ...
`, with `- ` as a fixed fallback name.
- **Primitive value** → `
value `.
Everything else mirrors the forward direction. `@_` keys become attributes, `#text` becomes text content, and a JSON array under a key produces repeated same-named siblings — the key name is reused, never singularized:
```javascript
// Convert JSON to XML in Node.js using fast-xml-parser
import { XMLBuilder } from 'fast-xml-parser';
const data = {
catalog: {
product: {
'@_id': 'P01',
name: 'Wireless Headphones',
price: { '@_currency': 'USD', '#text': '79.99' },
},
},
};
const builder = new XMLBuilder({
attributeNamePrefix: '@_', // @_ keys become attributes
textNodeName: '#text', // #text key becomes text content
ignoreAttributes: false, // process @_ keys
format: true, // pretty-print
});
console.log(builder.build(data));
//
//
// Wireless Headphones
// 79.99
//
//
```
One detail the builder handles for you: special characters in text and attribute values (`<`, `>`, `&`, `"`) are escaped to their entity references, so the output stays well-formed.
## FAQ
### How do XML attributes map to JSON?
Attributes become keys prefixed with `@_`, so `id="42"` turns into `"@_id": "42"`. This is the shared convention of `fast-xml-parser` and `xmltodict`, and the prefix never collides with element names because no valid element name starts with `@`.
### Why does XML to JSON keep numbers as strings?
Because the converter does no type coercion. Forcing `01234` into `1234` would drop a meaningful leading zero from ZIP codes, account numbers, and padded IDs. Keeping every value as a string is the safe default; coerce deliberately downstream where you know the type.
### Is XML to JSON conversion lossless?
No. Comments and processing instructions are discarded, namespace semantics are only partially preserved, and mixed-content ordering may not round-trip. When you need every byte preserved, use an XML Formatter to reformat the XML instead of converting it to JSON.
### How are repeated XML elements handled in JSON?
A single same-named child becomes an object; two or more become an array. Because the shape depends on sibling count, your consumer code should always normalize to an array so it handles both the one-item and many-item cases without crashing.
### What happens to XML namespaces when converting to JSON?
An `xmlns` declaration becomes an ordinary `@_xmlns` key, and the prefix stays inside the element-name string, as in `"soap:Body"`. The semantic binding of a prefix to a URI is not interpreted, so distinct namespaces can flatten together.
### How do I convert JSON back to XML?
Use the companion JSON to XML Converter. It applies the same `@_` and `#text` conventions in reverse, so attributes, text content, and arrays map back symmetrically. That symmetry is what makes a clean JSON → XML → JSON round-trip possible.
### Can I convert XML with multiple root elements?
No. Multiple top-level elements are not well-formed XML, so the parser rejects the input. Wrap the fragments in a single root element first — turn ` ` into ` ` — then convert.
## Conclusion
XML-to-JSON conversion is convention-driven, not a reformat. The rules are consistent across runtimes: attributes map to `@_` keys, mixed-content text to `#text`, repeated siblings to arrays, and values stay strings so leading zeros and precision survive. The traps to remember are the single-versus-array shape shift, silently dropped attributes, and the loss of comments and namespace semantics. None of those are bugs; all of them are predictable once you know the model mismatch behind them.
When you need a quick, private conversion, paste into the [XML to JSON Converter](/tools/xml-to-json) — it runs entirely in your browser. Validate the source first with the [XML Formatter](/tools/xml-formatter), and go the other direction with the [JSON to XML Converter](/tools/json-to-xml) when you need round-trip XML. For more on how data-format models shape conversion behavior, see the notes on [YAML and JSON differences](/blog/yaml-norway-problem-and-json-yaml-differences).
---
### The YAML Norway Problem and JSON-YAML Differences for Engineers
URL: https://go-tools.org/blog/yaml-norway-problem-and-json-yaml-differences
Why YAML reads "no" as false. Real K8s outages from string quoting. JSON vs YAML choices, indent rules & K8s manifest conversions explained.
# The YAML Norway Problem and JSON ↔ YAML Differences Engineers Should Know
It was a routine Helm deployment. The team had spent two days tuning a values.yaml file for a multi-region rollout. The chart templated a Kubernetes ConfigMap with locale metadata — including the country code for their Norwegian data center. Someone typed `country: NO` and committed it. The CI pipeline went green. The deployment went out.
Then the alerts came in.
The ConfigMap contained `country: false` instead of `country: "NO"`. Every downstream service that read the country field got a boolean instead of a string. The string comparison broke. The routing logic fell through to a default. Traffic that should have stayed in Norway ended up processed by the wrong regional endpoint.
The root cause was a single unquoted string in a YAML file. YAML 1.1 — the version that virtually all Kubernetes tooling uses — treats `NO` as a boolean `false`. It treats `YES`, `ON`, `OFF`, `Y`, `N`, `no`, `yes`, `on`, `off`, `y`, `n`, and a dozen more variants the same way. No warning. No error. Silently wrong.
JSON does not have this problem. `{"country": "NO"}` is always a string. YAML's implicit type coercion is both its greatest convenience and its most dangerous footgun.
This guide covers the full picture: why the Norway problem exists, what changed in YAML 1.2 (and why most tooling ignores it), how to write correct quoting strategies, the indentation rules that trip up newcomers, number precision traps, and four real-world conversion scenarios from Kubernetes manifests to Terraform plans. When you need to safely flatten a JSON value into YAML without this trap, our JSON to YAML converter auto-quotes Norway-prone strings automatically.
## JSON vs YAML — When to Use Which
Before diving into the Norway problem, it helps to understand what each format is actually optimized for. They are not interchangeable — each has a design center that makes it the better choice in specific contexts.
| Dimension | JSON | YAML |
|-----------|------|------|
| Syntax | Strict — braces, quotes, commas required | Flexible — indentation-driven, minimal punctuation |
| Type system | Explicit: string, number, boolean, null, array, object | Implicit — YAML 1.1 infers types from value shape |
| Human readability | Developer-friendly, machine-verifiable | Human-friendly, easy to hand-edit |
| Quote requirement | Strings always quoted | Most scalars can be unquoted (the source of Norway) |
| Comments | Not supported | Supported with `#` |
| Primary use | APIs, data exchange, modern config systems | Kubernetes, Docker Compose, Ansible, CI pipelines |
| Surprising parses | None — strict parsing | Yes — Norway, octal, timestamps |
| Schema enforcement | JSON Schema ecosystem | YAML Schema (less tooling) |
**JSON wins** when your data crosses system boundaries — REST APIs, message queues, database serialization. Machines parse it, machines generate it, and the strict syntax makes validation straightforward. Use a JSON Formatter to validate structure before sending.
**YAML wins** when humans are the primary authors. Kubernetes manifests, GitHub Actions workflows, Helm charts, Ansible playbooks — these are files developers read and edit dozens of times. The reduced punctuation and support for comments make them genuinely more maintainable than their JSON equivalents.
The problem arises at the boundary: when a tool generates JSON (like `kubectl get deploy -o json` or `terraform show -json`) and a human needs to version-control or edit the result as YAML. That conversion is where the Norway problem lives. Our YAML to JSON converter handles the reverse direction when you need to go back.
## The Norway Problem — Deep Dive
The Norway problem is not a bug. It is a feature of the YAML 1.1 specification behaving exactly as designed. Understanding why it was designed this way — and why so many systems still implement 1.1 — is the key to avoiding it.
### Why "no", "yes", "on", "off", "y", "n" Misparse
The YAML 1.1 specification defined a broad boolean type that was intended to be human-friendly. It recognized all of the following as `true` or `false`:
**True:** `y`, `Y`, `yes`, `Yes`, `YES`, `true`, `True`, `TRUE`, `on`, `On`, `ON`
**False:** `n`, `N`, `no`, `No`, `NO`, `false`, `False`, `FALSE`, `off`, `Off`, `OFF`
The intent was good: config files often use `yes`/`no` instead of `true`/`false` in English, and YAML wanted to support the natural way people write configuration. The problem is that `yes`, `no`, `on`, `off`, `y`, and `n` are also perfectly legitimate string values that mean something entirely different in most applications.
Here is the mismatch in concrete YAML:
```yaml
# YAML 1.1 (what most parsers implement)
country: NO # parses as: country: false ← DANGER
enabled: yes # parses as: enabled: true
restart: off # parses as: restart: false
language: y # parses as: language: true
shell: n # parses as: shell: false
# Correct — explicit string quotes override type inference
country: "NO" # parses as: country: "NO" ← safe
enabled: "yes" # parses as: enabled: "yes"
restart: "off" # parses as: restart: "off"
language: "y" # parses as: language: "y"
shell: "n" # parses as: shell: "n"
```
And the JSON comparison:
```json
{"country": "NO"}
```
In JSON, `NO` inside quotes is always and unconditionally a string. There is no implicit type inference. The strictness that makes JSON feel verbose is also what makes it safe.
Beyond boolean coercion, YAML 1.1 also implicitly converts:
- `123e4` → the number `1230000` (scientific notation)
- `0x1A` → the number `26` (hexadecimal)
- `0755` → the number `493` (octal — this one breaks Unix file permission strings)
- `2024-05-04` → a date object in many parsers (not just a string)
- `1_000_000` → the number `1000000` (underscore separator)
The Norway problem is really just the most famous member of a whole family of YAML implicit type coercions.
### YAML 1.1 vs 1.2 — What Changed
YAML 1.2 was published in 2009 — four years after YAML 1.1. Its primary goal was to bring YAML into strict alignment with JSON (since JSON is actually a valid YAML 1.2 subset) and to reduce the surprising implicit type conversions.
In YAML 1.2:
- Boolean is narrowed to exactly **`true` and `false`** (case-sensitive). That is it. `yes`, `no`, `on`, `off` are plain strings.
- Octal literals require the `0o` prefix (`0o755`) — the old `0755` form is a string.
- Timestamps are not implicitly parsed — `2024-05-04` stays a string unless you tag it explicitly.
- The specification itself is a JSON superset, meaning every valid JSON document is valid YAML 1.2.
On paper, YAML 1.2 solves the Norway problem entirely. In practice, the ecosystem barely moved.
| Library | Default spec | Norway risk |
|---------|-------------|-------------|
| PyYAML (Python) | YAML 1.1 | Yes — `yaml.safe_load` still parses `NO` as `False` |
| ruamel.yaml (Python) | YAML 1.2 (optional) | Configurable — safer by default |
| js-yaml (Node.js) | YAML 1.1 | Yes in older versions; newer versions have `FAILSAFE_SCHEMA` option |
| eemeli/yaml (Node.js) | YAML 1.2 | No — 1.2 by default, or explicitly version-selectable |
| gopkg.in/yaml.v2 (Go) | YAML 1.1 | Yes |
| gopkg.in/yaml.v3 (Go) | YAML 1.2 | Significantly safer |
| Kubernetes / Helm | YAML 1.1 (via Go yaml.v2) | Yes — historical, very difficult to migrate |
| Ansible | YAML 1.1 (via PyYAML) | Yes |
The reason migration is slow is backward compatibility. Systems that have relied on `yes`/`no` parsing as booleans for a decade cannot silently change that behavior without breaking existing configs. Kubernetes in particular is a massive installed base where changing YAML parsing semantics would be a cluster-wide breaking change.
**The practical conclusion:** assume YAML 1.1 semantics in any tool you did not explicitly configure otherwise. Always quote strings that could be misread as booleans, timestamps, or numbers.
### How Production Systems Get Bitten
The Norway country code is the most-cited example because it is counterintuitive — `NO` looks like an obvious abbreviation, not a boolean. But the pattern repeats across many real-world scenarios:
**IATA airport codes.** The Norwegian airport Harstad/Narvik has code `EVE`. Safe. Oslo Gardermoen is `OSL`. Also safe. But any application using YAML to store regional airport codes is one `no` route code away from a boolean false in production.
**Environment variable names.** `ON` is a perfectly valid environment variable value meaning "enabled" in some legacy systems. `OFF` is its counterpart. Migrating configs from shell scripts to YAML without quoting these values introduces silent type coercion.
**Email user fields.** A user whose first name or username is literally `n`, `y`, or any of the trigger words will serialize incorrectly if the application dumps YAML without proper quoting. This is particularly insidious because it fails for only a subset of users.
**Docker Compose restart policies.** The `restart_policy` field's value `"no"` means "do not restart." If it loses its quotes in a YAML round-trip, the value becomes `false`, and Docker Compose may interpret it as "no restart policy specified" or throw a validation error — either way, the container restart behavior is wrong.
**GitHub Actions `shell:` field.** The valid shell values are `bash`, `pwsh`, `python`, `sh`, `cmd`, `powershell`. None of these are Norway words. But someone who types `shell: yes` or `shell: on` as a placeholder during draft editing may be surprised when YAML turns it into a boolean before the validator even sees it.
The fix in all cases is the same: quote strings that are semantically strings, regardless of whether a human would recognize them as keywords. Our JSON to YAML converter applies this automatically — any value in the Norway-word list gets quoted in the output.
## String Quoting Strategy
Once you understand why Norway words mismatch, the solution is choosing the right quoting strategy for your use case. YAML supports three modes, each with different tradeoffs.
### Auto vs Double vs Single
**Auto quoting** (recommended for most conversions) lets the library decide when quotes are necessary. Values that would be misread without quotes — Norway words, numbers, timestamps, strings that look like YAML syntax — get quoted automatically. Everything else stays as a plain scalar. This produces the most readable output while remaining safe.
```yaml
# Auto mode output
name: Alice # plain — no ambiguity
country: "NO" # quoted — Norway word
age: 30 # plain — unambiguous number
created: "2024-05-04" # quoted — would otherwise parse as a date
port: "8080" # depends on library — some quote numeric-looking strings
```
**Double-quoted strings** wrap all string values in double quotes. This is explicit and auditable — any reader can see that all these values are strings without reasoning about the spec. The tradeoff is verbosity and reduced human readability, especially for deeply nested configs.
```yaml
# Double-quote mode
name: "Alice"
country: "NO"
replicas: "3" # even numbers become strings — may cause schema errors
```
Be careful: if your target schema expects a number and you serialize it as a quoted string, the YAML parser will correctly type it as a string, but Kubernetes or another strict consumer may reject the field as the wrong type.
**Single-quoted strings** are a YAML-only feature — JSON has no single-quote syntax. Single quotes are literal: no escape sequences inside them. The only special case is that a single quote inside a single-quoted string must be doubled (`''`). Single quotes are ideal for strings that contain backslashes or special characters that would need escaping in double quotes.
```yaml
# Single-quote mode
pattern: 'C:\Users\alice\Documents' # no escape needed
regex: '\d+\.\d+' # backslashes literal
```
For JSON-to-YAML conversions intended to round-trip back to JSON, prefer Auto or Double mode. Single-quoted strings introduce a YAML-specific syntax that requires a YAML-aware parser on the way back.
### Block Scalars (| and >)
YAML's block scalar syntax is genuinely useful for multi-line strings — something JSON handles awkwardly with `\n` escape sequences.
**Literal block scalar `|`** preserves newlines exactly:
```yaml
# Literal block — newlines kept
script: |
#!/bin/bash
set -euo pipefail
echo "Starting deployment"
kubectl apply -f manifest.yaml
# Equivalent JSON representation (unreadable)
# {"script": "#!/bin/bash\nset -euo pipefail\necho \"Starting deployment\"\nkubectl apply -f manifest.yaml\n"}
```
**Folded block scalar `>`** joins lines with spaces, turning each newline into a space (except blank lines, which become newlines):
```yaml
# Folded block — newlines become spaces
description: >
This service handles authentication
for the entire platform. It supports
OAuth2, SAML, and API key authentication.
# Result: "This service handles authentication for the entire platform. It supports OAuth2, SAML, and API key authentication.\n"
```
Block scalars shine for embedding TLS certificates, multi-line shell scripts, or SQL queries in YAML configs — scenarios where the JSON equivalent would be a long, escaped, one-liner that no human can read.
When converting from JSON to YAML, most converters (including ours) use Auto mode and represent multi-line strings with block scalars only when they detect embedded newlines. Single-line strings get flow scalars (quoted or plain). Use our JSON to YAML converter to see the output before committing it to a manifest.
## Indentation — 2 vs 4 Spaces, Tabs Forbidden
YAML's indentation rules are stricter than they look. The spec has one absolute rule and one convention that varies by ecosystem.
**The absolute rule: tabs are forbidden.** Every indentation level must use spaces. A tab character in a YAML file is a parse error in most parsers:
```yaml
# WRONG — tabs cause parse errors
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app # ← tab character here → ParseError
# CORRECT — spaces only
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app # ← two spaces
```
The error message you will see varies by library. In Python's PyYAML:
```
yaml.scanner.ScannerError: while scanning for the next token
found character '\t' that cannot start any token
```
In Go's yaml.v3:
```
yaml: line 4: found character that cannot start any token
```
Configure your editor to expand tabs to spaces for YAML files. In VS Code, add to your workspace settings: `"[yaml]": { "editor.insertSpaces": true, "editor.tabSize": 2 }`.
**The convention: 2 vs 4 spaces.** Both are valid. Ecosystem conventions differ:
| Ecosystem | Convention | Reason |
|-----------|-----------|--------|
| Kubernetes manifests | 2 spaces | Official docs and examples use 2 |
| Helm charts | 2 spaces | Follows K8s convention |
| Docker Compose | 2 spaces | Official compose spec examples |
| GitHub Actions | 2 spaces | Official workflow examples |
| Ansible playbooks | 2 spaces | Official documentation |
| Traditional configs | 4 spaces | Matches JSON beautify default |
For any file that will be consumed by Kubernetes or Docker Compose, use 2 spaces. For standalone config files that will only be read by humans and custom tooling, either works — just be consistent within a file. Our JSON to YAML converter defaults to 2-space indentation and lets you switch to 4 for projects that prefer it.
One more rule: child elements must be indented more than their parent, but the number of additional spaces can be any positive integer (1, 2, 3, 4...) — as long as it is consistent within a block. In practice, always use 2 or 4 for readability.
## Number Handling Across JSON ↔ YAML
Both formats support numbers, but the edge cases differ enough to cause production bugs.
### Precision Loss for Big Numbers
JavaScript's `Number` type is a 64-bit IEEE 754 float. It can represent integers exactly up to 2^53 − 1 = 9,007,199,254,740,991. Beyond that, integer precision is lost:
```js
// JavaScript precision loss — this is not a YAML problem, but it affects JSON parsing
JSON.parse('{"v": 9007199254740993}').v
// → 9007199254740992 (the 3 became 2 — one bit lost)
// Safe — within 2^53 range
JSON.parse('{"v": 9007199254740991}').v
// → 9007199254740991 (exact)
```
This matters for JSON-to-YAML conversion in JavaScript environments because the precision is already lost before YAML serialization begins. Kubernetes `metadata.resourceVersion` is a string field specifically because resource versions can exceed the safe integer range. Other fields that look like small numbers — `observedGeneration`, `uid` components — are safer, but any int64 field in a K8s response is potentially affected.
**Workarounds:**
- Use Python or Go for conversion pipelines involving large numbers — both handle arbitrary integers natively.
- In Node.js, use a JSON parser that supports BigInt: `JSON.parse(text, (_, v) => typeof v === 'number' && !Number.isSafeInteger(v) ? BigInt(v) : v)`.
- For fields that must round-trip without loss, serialize them as strings at the source.
- When reviewing converted YAML, look for fields like `resourceVersion`, `generation`, and timestamp-derived values.
### Octal & Hex Quirks
YAML 1.1 treats certain number-like strings as non-decimal integers:
```yaml
# YAML 1.1 parsing surprises
permissions: 0755 # parses as octal 493, not decimal 755
value: 0x1A # parses as hex 26, not string "0x1A"
# YAML 1.2 behavior
permissions: 0755 # stays as integer 755 (decimal) — octal requires 0o prefix
permissions: 0o755 # parses as octal 493 in both 1.1 and 1.2
# Safe for both specs — quote any leading-zero value
permissions: "0755" # always the string "0755"
```
The octal trap is particularly dangerous for Unix file permissions, IP address components with leading zeros (some network devices), and any numeric code that uses leading zeros for padding (ZIP codes, product codes). Always quote these values when writing YAML by hand, or ensure your converter quotes them — our JSON to YAML converter detects numeric strings from JSON and preserves their string type.
## Real-World Conversions
The Norway problem and quoting strategies become concrete when you apply them to real conversion scenarios.
### Kubernetes Manifest from JSON
The canonical workflow: `kubectl get deploy my-app -o json` gives you the live object as JSON. You want to clean it up (remove `status`, `creationTimestamp`, managed fields) and check it into git as a YAML manifest.
**Source JSON (abbreviated):**
```json
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "my-app",
"namespace": "production",
"labels": {
"app": "my-app",
"region": "NO"
}
},
"spec": {
"replicas": 3,
"selector": {
"matchLabels": { "app": "my-app" }
},
"template": {
"spec": {
"containers": [{
"name": "app",
"image": "registry.example.com/my-app:v1.2.3",
"env": [
{ "name": "REGION", "value": "NO" },
{ "name": "ENABLE_FEATURE", "value": "yes" }
]
}]
}
}
}
}
```
**Expected YAML output (with Norway protection):**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
labels:
app: my-app
region: "NO" # quoted — Norway word
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
spec:
containers:
- name: app
image: registry.example.com/my-app:v1.2.3
env:
- name: REGION
value: "NO" # quoted — Norway word
- name: ENABLE_FEATURE
value: "yes" # quoted — Norway word
```
Notice that `replicas: 3` is left unquoted — it is a legitimate integer that Kubernetes expects as a number. The Norway words in `labels` and `env` values are quoted. A naive converter that does not handle YAML 1.1 booleans would silently produce `region: false` and `value: false`.
After converting, validate with: `kubectl apply --dry-run=client -f manifest.yaml`. This catches schema errors without touching the cluster.
Try the conversion in our JSON to YAML converter — paste the JSON above and see Norway-safe output instantly. Use our YAML to JSON converter to verify the round-trip.
### Docker Compose from JSON
CI/CD pipelines sometimes generate Docker Compose configs programmatically from a JSON configuration store, then write them to disk as YAML for developers to read.
**Critical trap — restart policy:**
```json
{"restart_policy": "no"}
```
In Compose, `restart_policy: "no"` is a valid value meaning "never restart the container." Without quotes in YAML, this becomes `restart_policy: false`, which Docker Compose may either treat as the same semantic (falsy = no restart) or reject with a type validation error — behavior varies by Compose version. The quoting is mandatory.
**Also watch for:** Compose v3 `deploy.restart_policy.condition: "on-failure"` — the `on-failure` value contains the word `on`, but it is hyphenated and not in the trigger list, so it is actually safe. However, `condition: on` (without the `-failure`) would mismatch. Quote environment variable values in the `environment:` block if they could be Norway words.
Validate Compose files after conversion: `docker-compose config` parses and re-outputs the canonical form, surfacing type errors.
### GitHub Actions Workflow
GitHub Actions workflows are YAML files hand-edited by developers. The most common conversion scenario is reading workflow data from the GitHub API (which returns JSON) and converting it to a local YAML file for editing.
The key fields to watch:
```yaml
# SAFE — no Norway words in standard GitHub Actions
on: # "on" is a YAML key here, not a value — handled differently
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: |
npm install
npm test
env:
NODE_ENV: production # safe — not a Norway word
DEBUG: "off" # Norway word in value — needs quoting
```
Note: `on:` as a YAML key is special — the Norway problem applies to values, not keys. But `on` as a value (like `DEBUG: on`) would trigger the coercion. The `env:` block deserves particular scrutiny because environment variable values are strings, but many of them are short flags that could collide with Norway words.
For workflows that include `shell:` specifications, valid values (`bash`, `pwsh`, `sh`, `python`) are all safe from Norway coercion. Custom values should be quoted proactively.
### Terraform JSON Plan → YAML
`terraform show -json tfplan > plan.json` outputs a detailed JSON representation of what Terraform plans to create, modify, or destroy. Converting this to YAML makes it more readable for pull request reviews and compliance audits.
```bash
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
# Then convert with our tool or a library
```
The Terraform plan JSON is complex and deep. Key concerns when converting:
1. **Large integer IDs.** Cloud resource IDs (AWS account IDs, GCP project numbers) and computed attribute values can be large numbers. Convert via Python or Go to avoid float64 precision loss.
2. **Version constraint strings.** Terraform uses `~>`, `>=`, `<=` in provider version constraints. These are string values that YAML handles correctly as long as they are not Norway words — but `~>` is safe.
3. **Provider configuration values.** Terraform plan outputs can include configuration values for resources. If a boolean field defaults to `false` and is represented as `"no"` in some provider schema, that is a Norway risk on the way back to YAML.
4. **The `.sensitive_values` block.** Sensitive values are redacted as `true` booleans in the plan JSON. These survive conversion cleanly since `true` is not a Norway word in either YAML version.
The Terraform-to-YAML conversion is primarily for human review, not for feeding back into Terraform. Do not use YAML manifests as Terraform input — Terraform's native format is HCL, and its JSON input format is specific and documented separately.
## Code Examples — 4 Languages
### Node.js (eemeli/yaml + js-yaml)
The Node.js ecosystem has two dominant YAML libraries with meaningfully different Norway handling:
```js
// eemeli/yaml — recommended, YAML 1.2 by default, Norway-safe
import { stringify } from 'yaml';
import { readFileSync } from 'fs';
const jsonInput = readFileSync('input.json', 'utf8');
const data = JSON.parse(jsonInput);
// Default: YAML 1.2 — "NO" stays as "NO", no boolean coercion
const yamlOutput = stringify(data);
console.log(yamlOutput);
// region: NO ← safe in 1.2, but for maximum compatibility quote it explicitly
// Force YAML 1.1 behavior (for K8s/Helm environments that parse 1.1)
const yamlForK8s = stringify(data, { version: '1.1' });
// region: 'NO' ← auto-quoted because 1.1 would parse NO as false
console.log(yamlForK8s);
```
```js
// js-yaml — widespread, but YAML 1.1 semantics, Norway-risky without care
import yaml from 'js-yaml';
import { readFileSync } from 'fs';
const data = JSON.parse(readFileSync('input.json', 'utf8'));
// Default dump — Norway words may not be quoted
const unsafe = yaml.dump(data);
// region: NO ← will parse as false if re-read by a 1.1 parser!
// Safer: use a custom schema or force quoting
const safer = yaml.dump(data, {
schema: yaml.JSON_SCHEMA, // restricts to JSON-compatible types
noCompatMode: false,
lineWidth: -1,
quotingType: '"',
forceQuotes: false, // only quotes when necessary per JSON schema
});
```
For new projects, prefer `eemeli/yaml`. Its YAML 1.2 default is safer, its Document API gives fine-grained control over quoting, and it handles the round-trip fidelity better. For projects already using `js-yaml`, use the `JSON_SCHEMA` option to restrict to JSON-safe types. For a deeper look at filtering and transforming JSON before conversion, see the jq command-line cheat sheet for pre-processing patterns.
### Python (PyYAML + ruamel.yaml)
Python is the dominant language for Kubernetes tooling, Ansible, and data engineering pipelines — all heavy YAML users.
```python
import json
import yaml
import sys
# PyYAML — simple, standard, but YAML 1.1 by default
with open('input.json') as f:
data = json.load(f)
output = yaml.dump(data, default_flow_style=False, allow_unicode=True)
# country: 'NO' ← PyYAML is actually smart enough to auto-quote Norway words
# But it does NOT quote "yes", "no" (lowercase) in all configurations:
# enabled: 'yes' ← quoted
# tag: y ← may or may not be quoted depending on version
print(output)
```
```python
import json
import sys
from ruamel.yaml import YAML
# ruamel.yaml — round-trip fidelity, supports YAML 1.2, recommended for production
yaml_rt = YAML()
yaml_rt.default_flow_style = False
yaml_rt.width = 4096 # prevent unwanted line wrapping
yaml_rt.best_map_flow_style = False
with open('input.json') as f:
data = json.load(f)
yaml_rt.dump(data, sys.stdout)
# Preserves key order, handles Norway words correctly, supports anchors on round-trip
```
For Ansible and Kubernetes automation scripts where you are converting JSON API responses to YAML manifests, `ruamel.yaml` is the safer choice. PyYAML is fine for simple scripts where you control the input data and have verified no Norway words appear.
If you use JSON5 or JSONC config files (with comments) before conversion, strip the extensions first — see the JSON5 and JSONC formatting guide for compatible parsers.
### Go (gopkg.in/yaml.v3)
Go is the language of the Kubernetes ecosystem itself — `kubectl`, Helm, Argo, Flux, and most K8s operators are written in Go.
```go
package main
import (
"encoding/json"
"fmt"
"os"
"gopkg.in/yaml.v3"
)
func main() {
// Read JSON input
jsonBytes, err := os.ReadFile("input.json")
if err != nil {
panic(err)
}
// Unmarshal JSON into a generic map
var data map[string]interface{}
if err := json.Unmarshal(jsonBytes, &data); err != nil {
panic(err)
}
// Marshal to YAML — yaml.v3 uses YAML 1.2 semantics
yamlBytes, err := yaml.Marshal(data)
if err != nil {
panic(err)
}
fmt.Println(string(yamlBytes))
// country: "NO" ← yaml.v3 quotes Norway words correctly
// replicas: 3 ← integers stay integers
// enabled: true ← booleans stay booleans
}
```
`yaml.v3` is a significant improvement over `yaml.v2` for Norway safety. The v2 library followed YAML 1.1 and would write `NO` without quotes; v3 quotes ambiguous values correctly. If you are maintaining an older Go project that uses v2, upgrade to v3 — the API is largely compatible and the safety improvement is worth the migration.
For type-safe conversion with Go structs (rather than `map[string]interface{}`), use struct tags:
```go
type DeploymentLabels struct {
App string `yaml:"app" json:"app"`
Region string `yaml:"region" json:"region"`
}
// yaml.Marshal on a struct field containing "NO" will quote it correctly in v3
```
### Bash CLI (yq + jq)
For shell scripts and quick one-off conversions, `yq` (Mike Farah's version, `mikefarah/yq`) converts JSON to YAML in a single command:
```bash
# Install yq
brew install yq # macOS
sudo wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
chmod +x /usr/local/bin/yq # Linux
# Convert JSON file to YAML
yq -P < input.json > output.yaml
# Convert from kubectl JSON output
kubectl get deploy my-app -o json | yq -P > manifest.yaml
# Pipe through jq first to filter/transform, then convert to YAML
kubectl get deploy my-app -o json \
| jq 'del(.status, .metadata.creationTimestamp, .metadata.managedFields)' \
| yq -P > clean-manifest.yaml
```
The `jq | yq` pipeline is a powerful pattern: use `jq` for JSON manipulation (filtering fields, reshaping structure, querying values) and `yq -P` as the final YAML serializer. For `jq` patterns, see the jq command-line cheat sheet for 30 real-world patterns including `kubectl` and `aws` integrations.
**Norway caution with yq:** `yq` (mikefarah) respects the input type from JSON — a JSON string `"NO"` in the input will serialize as a YAML string with quotes. But if you generate YAML directly with `yq` (not from JSON input), you must quote Norway-word values explicitly. Use our YAML to JSON converter to validate the round-trip after `yq` output.
## Edge Cases & Gotchas
Beyond the Norway problem, JSON ↔ YAML conversion has several edge cases that trip up experienced engineers:
1. **Multi-document YAML (`---` separator).** A single YAML file can contain multiple documents separated by `---`. JSON has no equivalent concept. When converting multi-document YAML to JSON, most tools either take the first document only, merge all documents into an array, or error out. When converting JSON to YAML, a single `---` document header is added by convention. Decide and document your behavior explicitly for pipelines that may encounter multi-document files.
2. **YAML anchors and aliases.** YAML supports `&anchor` definitions and `*alias` references for DRY configs. When converting YAML to JSON, anchors must be expanded — the resulting JSON may be much larger than the source YAML. When converting JSON to YAML, the converter cannot reconstruct anchors that did not exist in the original. Aliases are a YAML-only feature.
3. **Timestamp implicit parsing.** YAML 1.1 parsers convert `2024-05-04` and `2024-05-04T12:00:00Z` to language-native date objects, not strings. When this date object is serialized back to JSON, the output depends on the library: some output ISO strings, some output Unix timestamps, some output null. Round-tripping dates through YAML without explicit string quoting (`"2024-05-04"`) can silently change the format.
4. **The `!!binary` tag.** YAML can embed base64-encoded binary data with the `!!binary` tag. JSON has no binary type — binary must be a base64 string. When converting YAML with `!!binary` fields to JSON, decode to base64 string. When converting back, you cannot reconstruct the binary tag without knowing the schema. Kubernetes uses `!!binary` for some secret values.
5. **Key type collisions.** JSON requires object keys to be strings. YAML allows keys of any type — integer keys, boolean keys, even complex object keys. A YAML file with `true: value` or `1: value` as keys cannot be faithfully represented as JSON. Most converters stringify the keys, but the semantics change.
6. **Null representation variance.** In YAML, `null`, `~`, `Null`, `NULL`, and an empty value all mean null. In JSON, only `null` is null. When converting YAML to JSON, all of these normalize to `null`. But when converting JSON back to YAML, the null representation choice matters — `~` is more compact, `null` is more explicit. Pick one and stick to it.
7. **Sort order changes.** JSON objects technically have no defined key order (though most parsers preserve insertion order). YAML mappings similarly have no required order. But some YAML libraries sort keys alphabetically by default when serializing. This can cause large diffs in version control if the source JSON used a different order. Configure `sort_keys=False` in PyYAML (`default_flow_style=False` alone does not prevent sorting) and equivalent options in other libraries.
## When NOT to Convert
Conversion is not always the right answer. Here are the scenarios where staying in the original format is the better choice:
**Do not convert YAML to JSON if the YAML contains comments that document business logic.** YAML comments are not part of the data model — they disappear in any serialization to JSON. If a Kubernetes manifest has comments explaining why a specific resource limit was chosen or why a security policy exception was made, converting to JSON destroys that documentation. Keep the YAML.
**Do not auto-convert configs in CI pipelines without round-trip tests.** If your pipeline converts JSON to YAML and then applies the YAML to a cluster, add a round-trip validation step: YAML back to JSON, then compare with the original. This catches type coercion surprises before they reach production.
**Do not convert just because a tool outputs JSON.** `kubectl`, `aws`, `terraform`, and `docker inspect` all output JSON, but most of these tools also accept YAML as input. Before building a conversion step, check whether the target tool can directly accept YAML input — most modern DevOps tools can. Our YAML to JSON converter is most useful when you specifically need JSON for a tool that does not accept YAML.
**Do not convert if the schemas differ.** If your JSON uses `camelCase` keys and your YAML consumer expects `snake_case` (or vice versa), you need a transform step in addition to a format conversion. A bare format conversion will produce syntactically correct but semantically wrong YAML. Address the schema mapping explicitly.
**Do not keep both formats in sync manually.** If you are maintaining a `config.json` and a `config.yaml` that are supposed to be equivalent, you will drift. Pick one canonical format and derive the other automatically — or better, pick one format and eliminate the duplication.
## FAQ
### Does the YAML Norway problem still affect modern systems?
Yes — it is pervasive in the ecosystem. Kubernetes and Helm use Go's `yaml.v2` library (YAML 1.1 semantics) in significant parts of their codebases. Ansible uses PyYAML (YAML 1.1). GitHub Actions workflows are parsed by GitHub's internal YAML parser which has its own behavior. Most CI/CD YAML files in the wild are processed by YAML 1.1 parsers. Assume 1.1 semantics until you have verified otherwise.
### Why would I convert JSON to YAML if YAML is harder to parse?
The conversion is not about parser difficulty — it is about human editability. JSON is ideal for machines; YAML is ideal for humans who need to read, edit, and review configuration files. A Kubernetes manifest checked into git, reviewed in pull requests, and hand-tuned by engineers should be YAML. The same manifest retrieved from the API for programmatic processing should be JSON. Our JSON to YAML converter bridges the two.
### Can I round-trip JSON ↔ YAML losslessly?
With caveats, yes — for JSON-compatible data. JSON is a subset of YAML 1.2, so any valid JSON document is valid YAML 1.2. Going JSON → YAML → JSON should be lossless for any data without implicit type coercion. The Norway problem means a JSON string `"NO"` could survive the forward pass only if the converter quotes it, and then survive the return pass only if the YAML parser respects the quotes. Use a YAML 1.2 library for both directions to guarantee lossless round-trips.
### What is the safest YAML library for production?
For Python: `ruamel.yaml` configured for YAML 1.2. For Node.js: `eemeli/yaml` (the `yaml` package on npm). For Go: `gopkg.in/yaml.v3`. All three implement YAML 1.2 semantics or have explicit YAML 1.2 modes and handle Norway words correctly. Avoid YAML 1.1 libraries in new projects. If you must use a 1.1 library (PyYAML, js-yaml, yaml.v2) for compatibility reasons, always quote Norway-prone strings explicitly.
### Does Kubernetes manifest YAML support comments after JSON conversion?
No — comments cannot be recovered from JSON. JSON has no comment syntax, so there is nothing to convert. When you run `kubectl get deploy -o json` and convert the output to YAML for git storage, the resulting YAML will have no comments. Comments in a Kubernetes manifest must be written by a human after the conversion. This is one reason why keeping the hand-authored YAML as the canonical source is often preferable to round-tripping through the JSON API.
### How do I handle big integers like resourceVersion or nanosecond timestamps?
Kubernetes `metadata.resourceVersion` is a string field deliberately — the Kubernetes team knew that JSON parsers in JavaScript and other float64-based runtimes would lose precision on large integers. Always treat it as a string. For genuinely numeric large integers (like nanosecond epoch timestamps in some tracing systems), use Python's `int` type, Go's `int64`, or Node.js `BigInt` for parsing. Never pass them through `JSON.parse()` in JavaScript without a custom reviver function. When converting to YAML, these large integers are safe — YAML has no precision limit for integers. The danger is in the round-trip back through JavaScript's JSON parser.
### Is YAML 1.2 widely adopted yet?
Unevenly. The major language libraries have been migrating: Go's yaml.v3, Python's ruamel.yaml, and Node.js's eemeli/yaml all support or default to YAML 1.2. But Kubernetes, Ansible, and much of the DevOps ecosystem still runs on YAML 1.1 parsers due to the backward-compatibility cost of migration. YAML 1.2 adoption in new projects is recommended, but assume 1.1 for any system you did not configure yourself.
### Should our team standardize on JSON or YAML for configs?
Standardize on purpose, not on format. Use JSON for configs consumed by code (API request bodies, SDK config files, programmatic tooling). Use YAML for configs consumed by humans (Kubernetes manifests, CI pipelines, deployment configs, Ansible playbooks). Avoid mixing the two for the same config — pick one representation per config type and automate the conversion if you need both. When you do need to convert, both our JSON to YAML and YAML to JSON converters run entirely in your browser — no data leaves your device.
## Try It Now
Ready to convert a real file? Try our JSON to YAML converter for sanitizing JSON into safe Kubernetes YAML — it auto-quotes Norway words (`NO`, `yes`, `on`, `off`, and the full YAML 1.1 boolean list) and lets you choose 2-space or 4-space indentation. For the reverse direction, our YAML to JSON converter handles anchors, aliases, and multi-document YAML. Both tools run entirely in your browser — your data never leaves your device, which matters when you are working with production Kubernetes manifests or Terraform plans that contain sensitive resource configurations.