PostSiete

X's character limit is a budget, not a length

· 2 min read

Almost every tool that counts characters for X counts them wrong, including some large ones. The reason is that text.length is an obvious thing to reach for and is not what X measures.

The rule

X does not count characters. It counts weighted characters, and the weights are not all one.

Characters in a small set of ranges — broadly Latin letters, digits, and common punctuation — weigh 1. Everything else weighs 2. That includes emoji, and it includes most non-Latin scripts: Chinese, Japanese, Korean, Arabic, Cyrillic, Devanagari.

The consequence is that the same sentence has a different cost depending on the language it is written in, and a post that fits in English may not fit in Japanese. If you are publishing in more than one language, this is not a detail.

URLs cost a flat 23

Every URL counts as 23 characters, no matter its actual length. A nine-character link and a ninety-character link cost exactly the same, because X shortens both through its own wrapper before measuring.

Two things follow. Shortening a URL before posting saves you nothing at all — the character cost is identical and you have made the link less trustworthy-looking for no gain. And a post that looks 40 characters over the limit in your editor may be comfortably under it, if the overrun is a long URL.

Why tools get it wrong

Because the naive implementation is one property access, and it is right often enough to ship. An all-ASCII post with no links gives the same answer either way, and most posts during development are all-ASCII posts with no links.

It breaks on exactly the posts that matter: the one with an emoji in the hook, the one with a link to the thing you are announcing, the one written in the language most of your audience reads.

What correct looks like

Count code points, not UTF-16 units — an emoji is frequently two units and one character. Assign weight 1 to the light ranges and 2 to everything else. Replace each URL with a flat 23 before counting the remainder. Compare the total against 280.

And then — this is the part that is easy to get wrong even after the arithmetic is right — make sure the number you show the user is the number you actually send. A counter that measures the text in the editor while the publishing code appends hashtags on top is not a rounding error. It is two different notions of "the post", and the user only finds out which one was real after it is public.

We shipped that bug, found it while publishing, and fixed it by making one function the single source for both the counter and the publisher. The test that guards it runs the browser's copy of the rule and the server's copy over the same inputs and fails the build if they ever disagree.

280 is a budget. Spend it deliberately.