The guide
A character limit is four different rulers: how to fit one bio into every box
Clusters, code points, UTF-16 units and bytes give four honest answers to one question, so a bio that fits one box can be cut in half by the next.
- 1 cluster, 25 bytes one family emoji: 7 code points and 11 UTF-16 units
- 4 ways to count clusters, code points, UTF-16 units and UTF-8 bytes
- 256 and 2,560 clusters and bytes in one open profile schema, both enforced
- 1 or 2 what a line break costs, depending on LF or CR LF
Read the full guide Fold the guide
Every profile box asks the same polite question: how long is your bio? It sounds like a question with one answer. It is really four questions wearing one coat, and the answer you get depends on which ruler the system behind the box happens to hold.
That matters more than it seems. A bio that fits comfortably in one place can be refused in the next, or worse, accepted and silently cut in the middle of a word or the middle of an emoji. The cure is not a better guess at the limit. It is knowing what is being counted, and writing the bio so that it can shrink on purpose instead of being chopped by accident.
Four honest answers to one question
Start with what a reader sees. A person looking at a screen counts what the Unicode world calls grapheme clusters: the letter with its accent, the flag, the emoji family, each taken as one thing. Browsers expose this unit through Intl.Segmenter with the grapheme setting, which splits a string into exactly those user-perceived pieces. The W3C character model makes the same point from the other side: grapheme clusters match where a reader believes one character ends and the next begins more closely than raw code points do.
Underneath the clusters sit code points, the numbered characters of Unicode. An accented letter can be one code point or two, a base letter followed by a combining mark. A flag is always two.
Below those sit the units a program actually stores. A JavaScript string’s length property counts UTF-16 code units, and a character outside the Basic Multilingual Plane takes two of them, which is why most emoji count as two before anything else happens. The maxlength attribute on an HTML input or textarea measures length in the same UTF-16 units, so the browser’s own limit on a form field already disagrees with what the reader sees.
Last come bytes. RFC 3629 lays out the arithmetic for UTF-8 plainly: one byte below U+0080, two below U+0800, three for the rest of the Basic Multilingual Plane, and four beyond it. Plain English letters cost one byte each. Most accented Latin, Greek and Cyrillic letters cost two. A common Chinese or Japanese character costs three. An emoji outside the basic plane costs four for each code point it holds.
None of these four answers is wrong. They measure different things for different reasons. A person cares about clusters, a script sees UTF-16, a database column or a network field often sees bytes, and code points sit in between as the vocabulary everyone else is built on. Those are the 4 ways to count, and a careful bio tool should show all four side by side rather than pick one and hide the others.
Emoji are several characters wearing one face
The gap between those rulers is widest for emoji, because so many emoji are sequences. The Unicode emoji data files list the kinds. A flag is a pair of regional indicator letters. A skin tone is a modifier added after a base emoji. A keycap is a digit, a variation selector and an enclosing keycap mark. The largest group is the zero width joiner sequence, where several emoji are glued together with an invisible joiner so that a capable font draws them as one picture.
Take the family of a man, a woman, a girl and a boy, written as one joined sequence. It is 1 cluster, 25 bytes: four emoji of four bytes each plus three joiners of three bytes each. The same picture is 7 code points and 11 UTF-16 units. A box that counts clusters charges you one. A box that counts bytes charges you twenty-five.
The joiner itself is a quiet hazard. Inside an emoji sequence it does honest work. Pasted somewhere else, from a template or a copied profile, it does nothing visible and still costs three bytes. The same goes for a no-break space, a byte order mark or a direction control. These characters cannot be seen, so the only way to find them is to list them by code point, which is what a careful counter should do.
Accents raise a subtler problem. The letter e with an acute accent can be stored as one precomposed code point or as an e followed by a combining acute. They look identical, and Unicode treats them as canonically equivalent. String normalization exists to pick one form: NFC composes, NFD decomposes, and the K forms also fold compatibility characters such as ligatures and circled letters. One accented word in a bio can therefore change the code point count by one and the byte count by one, depending only on how your keyboard or your last copy and paste happened to store it. The count changes. The words do not.
Line breaks join the list. A textarea’s maxlength counts a line break as one, but the HTML standard says that when a form is submitted, every line break in a value is sent as a carriage return followed by a line feed. A bio with five lines can gain five units between the box you typed in and the server that checks it. Under the cluster rule, that same CR LF pair still counts as one, because the grapheme rules keep the pair together. So a line break costs 1 or 2, and only the system knows which.
A limit in bytes is not a limit in letters
It is tempting to treat all of this as edge cases. The open-source world shows that it is not. Mastodon’s source code sets a profile note limit of 500 and checks it by counting grapheme clusters, after first replacing every link with a fixed run of 23 characters, so a long link and a short one cost the same. That is a cluster ruler with a link discount built in.
The AT Protocol profile schema takes a different route. Its description field carries two limits at once: 256 grapheme clusters and a maximum length of 2,560, which the Lexicon specification defines in UTF-8 bytes. A bio must pass both. For ordinary Latin text the byte limit is never reached. For a bio written mostly in emoji sequences it can arrive first, which is exactly why the schema states both. Those numbers, 256 and 2,560, are what the code and the specification said when this was written; a running service can change its rules, so they are a reason to check, not a promise.
Most commercial services do not publish their counting rule in a form this page can cite. The honest response is not to invent one. Treat every other limit as a budget you set yourself, write down which ruler it uses, and check the real box before you rely on it.
What goes wrong when a system ignores all this is easy to show. A naive cut takes the first so many units and throws the rest away. If the units are UTF-16, it can stop between the two halves of a surrogate pair. If they are bytes, it can stop partway through a multi-byte character. Either way the text is no longer well formed. A lone half of a surrogate pair is drawn as the replacement character U+FFFD, a diamond with a question mark, and TextEncoder swaps it for the same character before sending. Half a flag is worse in a quieter way: it survives as a lonely regional indicator letter.
Writing a bio that can shrink
The alternative is to stop thinking of the bio as one string. Think of it as parts: a name line, a line about what you do, a piece of proof, a call to action, a link and a few hashtags. Give each part a shorter form you wrote yourself. Then decide, once, which parts matter most.
With that in place, fitting becomes a rule rather than a gamble. Start with everything in full. If it is too long, switch the least important parts to their short forms one at a time and stop as soon as it fits. If that is not enough, drop parts, least important first. Then walk back up the priority list and restore anything that will now fit. Nothing is cut inside a word unless you allow it, and nothing is ever cut inside a cluster.
Here is a worked example you can repeat in the tool on this page. The sample bio has six parts, including a name line with a woman artist emoji, a flag, a waving hand with a skin tone, a hashtag with a decomposed accent and two Chinese characters. In full it is 223 clusters, 228 code points, 235 UTF-16 units and 257 bytes.
Against a 256 cluster budget it fits with 33 to spare. Against a form field of 136 UTF-16 units with CR LF line ends, a naive cut lands between the two regional indicators of the flag and leaves half of it behind. The fitter instead uses five short forms and keeps the link in full, for 123 units. Against an 80 byte budget it keeps the short name, the short description, the full link and one hashtag, drops the proof and the call to action, and uses 77 bytes.
The order you choose is the real decision. If the link matters more than the name, say so, and the name will be shortened first. If a part has no short form, it is kept in full or dropped, never mangled. And every version records what it gave up, so you can see the trade instead of discovering it later.
Check where you post
A counter on a separate page can be exact about its own rules and still be wrong about someone else’s. Services rewrite text before counting: they shorten links, expand mentions, trim spaces or normalize accents. Some of those steps are documented in open code; most are not. The safest habit is simple. Count with the ruler you believe the box uses, keep a version for each box you care about, and paste the result into the real field once to watch what it does.
This method cannot tell you how a bio will look in a given font, how wide an emoji will be on a given phone, or whether a reader will care. It counts and it fits. That is a narrower job than writing, but it is the part that is pure arithmetic, and arithmetic is worth getting exactly right.
Sources
Every source consulted for this guide. Links checked October 20, 2026.
- Intl.Segmenter MDN Web Docs (CC BY-SA 2.5)
- String: length MDN Web Docs (CC BY-SA 2.5)
- String.prototype.normalize() MDN Web Docs (CC BY-SA 2.5)
- String.prototype.toWellFormed() MDN Web Docs (CC BY-SA 2.5)
- TextEncoder: encode() method MDN Web Docs (CC BY-SA 2.5)
- maxlength HTML attribute MDN Web Docs (CC BY-SA 2.5)
- HTML Standard: form control infrastructure WHATWG (CC BY 4.0)
- UTF-8, a transformation format of ISO 10646 (RFC 3629) IETF, RFC Editor
- Character Model for the World Wide Web 1.0: Fundamentals W3C
- Emoji Sequence Data for UTS #51 (emoji-sequences.txt, version 18.0) Unicode Consortium (Unicode License v3)
- Emoji ZWJ Sequences for UTS #51 (emoji-zwj-sequences.txt, version 18.0) Unicode Consortium (Unicode License v3)
- GraphemeBreakTest-17.0.0.txt Unicode Consortium (Unicode License v3)
- mastodon/app/models/account.rb Mastodon project on GitHub (AGPL-3.0)
- mastodon/app/validators/note_length_validator.rb Mastodon project on GitHub (AGPL-3.0)
- bsky/lexicons/app/bsky/actor/profile.json bluesky-social/bsky on GitHub (MIT or Apache-2.0)
- Lexicon - AT Protocol AT Protocol specification (CC BY)