Skip to content

Engineering

How X (Twitter) counts characters: emoji and the URL 23-character rule

X's (formerly Twitter's) 280-character limit is not a plain character count: every URL costs a flat 23 characters and every emoji costs 2. This walks through the real weighting rules SkedCast's own caption counter (countChars, in @skedcast/shared, the same function that powers /tools/caption-counter) implements — and gives real, testable input/output pairs rather than invented examples.

All articles

By The SkedCast Team · Updated · 6 min read

Key takeaways

  • Every URL in a post — regardless of its actual length — is charged a flat 23 characters, matching X's own t.co link-shortening cost.
  • Every emoji cluster (including multi-code-point sequences like a flag, a keycap digit, or a family emoji joined with zero-width joiners) is charged 2, never more, never based on how many underlying code points it contains.
  • Every other code point is charged 1 inside X's own weight-1 ranges (which covers Latin, Cyrillic, Hebrew and most punctuation) and 2 outside them — which is why CJK ideographs, Kana and Hangul syllables each cost 2, not 1.
  • The text is NFC-normalized before counting, so a decomposed accented character (a base letter plus a separate combining mark) counts the same as its single precomposed code point.

Why a plain character count is wrong for X

Most platforms count a caption by its grapheme length — the number of user-perceived characters, correctly handling multi-code-point emoji so a family emoji counts as one "character" rather than five. X does something different: it applies its own weighting scheme (originally published as twitter-text) on top of grapheme counting, so the number that actually gates a 280-character post can differ substantially from what a naive length check reports.

X (Twitter) character count rules: emoji and the 23-character URL

First, the text is normalized to NFC — Unicode's canonical composed form — so a decomposed character (a base letter followed by a separate combining accent mark, which visually renders identically to its precomposed form) counts as the single composed code point it displays as, not as two separate units.

Second, every URL is extracted and charged a flat cost of 23 characters each, regardless of the URL's actual length — this mirrors X's own t.co automatic link-shortening, which rewrites every link to a fixed-length wrapped URL before display.

Third, every remaining grapheme cluster is checked: if it matches an emoji pattern, it costs exactly 2, no matter how many underlying Unicode code points make it up (a flag emoji built from two regional-indicator code points, a keycap digit built from a digit plus a combining enclosing mark, or a family emoji joined by zero-width joiners all cost 2, not more). Anything that is not an emoji cluster is charged code-point by code-point: 1 for a code point inside X's documented weight-1 ranges, 2 for everything outside them — and the weight-1 ranges cover Latin, Cyrillic, Hebrew, and most general punctuation, but NOT CJK ideographs, Kana, or Hangul syllables, each of which costs 2.

Real test vectors

These are pulled directly from SkedCast's own unit test suite for this function, not invented for this article — each one is a real assertion that has to keep passing:

  • "see https://example.com/a/very/long/path?x=1" → 27 (a 4-word prefix of "see " counts normally; the URL, however long, collapses to a flat 23)
  • 140 repeated 😀 emoji → 280 (each emoji costs exactly 2, so 140 × 2 = 280 — the maximum a post built entirely of emoji can hold)
  • 174 repeated あ (hiragana) → 348 (each kana character costs 2: 174 × 2 = 348)
  • "漢" (a single CJK ideograph) → 2
  • "한" (a single Hangul syllable) → 2
  • "Привет" (Cyrillic, 6 characters) → 6 (Cyrillic sits inside the weight-1 ranges, so it counts like Latin text)
  • "é" as a single precomposed code point → 1, and the same visual character built as a decomposed base-plus-combining-mark pair → also 1, after NFC normalization
  • "🇯🇵" (a flag built from two regional-indicator code points) → 2, not 4
  • "👨‍👩‍👧" (a three-person family emoji joined by zero-width joiners) → 2, not 6 or more
  • "hi https://a.co" → 26 (the 3-character prefix "hi " plus the flat 23-character URL cost)

Why this matters beyond X specifically

A caption-length tool that only counts grapheme clusters will systematically under-report how close a caption is to X's real 280 limit whenever it contains URLs, CJK/Kana/Hangul text, or a large share of emoji — and a caption that reads as "well under the limit" by a naive count can fail to publish. SkedCast's own composer counts through this exact function everywhere a character count is shown, so the number displayed while composing can never disagree with the number the publish call itself enforces.

xtwittercharacter countunicodeengineering

FAQ

Does X count every URL by its actual length?
No — every URL is charged a flat 23 characters regardless of how long it actually is, mirroring X's own t.co automatic link-shortening, which rewrites every posted link to a fixed-length wrapped form.
How many characters does an emoji cost on X?
Exactly 2, regardless of how many underlying Unicode code points the emoji is built from — a simple emoji, a flag, a keycap digit, and a multi-person family emoji joined with zero-width joiners all cost 2, never more.
Why do Japanese, Chinese and Korean text cost more per character on X than English?
Because CJK ideographs, Kana characters and Hangul syllables fall outside X's documented weight-1 code-point ranges and are charged 2 each, while Latin, Cyrillic, Hebrew and most punctuation fall inside those ranges and are charged 1 each.
Does it matter if an accented character is typed as one code point or two?
No — the text is normalized to its canonical composed form (Unicode NFC) before counting, so a decomposed character (a base letter plus a separate combining accent mark) counts the same as its single precomposed equivalent.
Is this the same counting rule other platforms use?
No — most other platforms simply count grapheme clusters (correctly treating a multi-code-point emoji as one character) with no URL-collapsing or CJK/Kana/Hangul weighting on top. X's weighting scheme, based on its own published twitter-text library, is distinctive to X.
Where can I check my own caption against these exact rules?
SkedCast's free caption counter tool applies this same weighted-counting function live, per platform, so a caption's real X count (not a naive character count) is visible while writing it.

Ready to broadcast everywhere?

Sign up free — no credit card. You land on the Free plan, and you can start a one-time 7-day Studio trial from your workspace whenever you are ready. Connect your first accounts, import a batch, and watch one post fan out across every platform.