Why Character Counting Is More Complex Than It Looks
Counting characters seems simple — until you encounter emoji, Korean text, or Arabic script. The number of "characters" a user sees can differ significantly from the number of bytes stored, the number of code points in Unicode, or the number of characters a platform counts for its limit. Understanding these distinctions prevents bugs and ensures your text fits where it needs to.
Unicode Code Points vs Bytes
Unicode assigns a unique number (code point) to every character. The code point for "A" is U+0041, for "한" is U+D55C, and for the emoji 😀 is U+1F600. Unicode itself is just a mapping of numbers to characters — it says nothing about how those numbers are stored in memory.
UTF-8 is the most common encoding for the web. It encodes each Unicode code point using 1 to 4 bytes:
ASCII characters (U+0000 to U+007F) use 1 byte. Latin characters with diacritics use 2 bytes. Korean, Chinese, Japanese characters use 3 bytes. Emoji and rare symbols use 4 bytes.
This means "Hello" is 5 bytes, "안녕" is 6 bytes (3 bytes × 2), and "😀" is 4 bytes — even though each visually appears to be just a few "characters."
The Emoji Problem: Surrogate Pairs and Grapheme Clusters
Emoji introduce additional complexity. In JavaScript, strings are internally encoded as UTF-16. Most emoji fall outside the Basic Multilingual Plane (BMP) and require two UTF-16 code units, called a surrogate pair.
// JavaScript example
"😀".length // → 2 (surrogate pair, not 1!)
[..."😀"].length // → 1 (spread operator splits by code point)
// Family emoji with ZWJ sequences
"👨👩👧👦".length // → 11 (multiple code points joined by ZWJ)
[..."👨👩👧👦"].length // → 7 (still not 1 visual character!)
// Use Intl.Segmenter for accurate visual character count
const segmenter = new Intl.Segmenter();
[...segmenter.segment("👨👩👧👦")].length // → 1 (correct!)
A grapheme cluster is what a user perceives as a single character. Family emoji, flag emoji, and skin tone modifier combinations are all single grapheme clusters but multiple code points. For most practical purposes — like counting characters for a tweet or SMS — you want grapheme cluster count.
Platform Character Limits
Each platform counts characters differently, which adds to the confusion.
X (Twitter): Uses a weighted character count. Most characters = 1, URLs (any length) = 23 characters. The limit is 280 weighted characters. Emoji count as 2 characters. Their API returns weighted_character_count separately from display_text_range.
Instagram: Caption limit is 2,200 characters (Unicode code points). Hashtags count toward this limit. The bio limit is 150 characters.
SMS: Standard SMS (GSM-7) supports 160 characters per message. If your message contains any character outside the GSM-7 set (including most emoji and non-Latin scripts), it switches to UCS-2 encoding and the limit drops to 70 characters per segment. A single emoji can cause a 160-character message to split into multiple segments.
Meta description: Google typically displays 155–160 characters. Beyond that, text is truncated with "...". The actual limit is pixel-based (~920px wide), so wide characters like "W" truncate sooner than narrow ones.
Database Storage Considerations
Database column lengths interact with encoding in non-obvious ways.
MySQL: VARCHAR(255) stores up to 255 characters (code points), not 255 bytes. With utf8mb4 encoding (required for emoji), each character can use up to 4 bytes, so 255 characters = up to 1020 bytes. The row size limit is 65,535 bytes, which limits how many large VARCHAR columns you can have per table.
MySQL utf8 vs utf8mb4: MySQL's utf8 charset only supports up to 3 bytes per character — which means it cannot store emoji (which require 4 bytes). Always use utf8mb4 for tables that may contain emoji or any Unicode character above U+FFFF.
-- Safe default for any text that might contain emoji
CREATE TABLE posts (
content VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
);
PostgreSQL: Uses UTF-8 natively and handles all Unicode correctly without special configuration. VARCHAR(255) limits by character count (code points).
Counting in Different Programming Languages
JavaScript: str.length returns the number of UTF-16 code units, not grapheme clusters. For accurate user-visible character count, use Intl.Segmenter (modern browsers and Node 16+) or the spread operator [...str].length (counts code points, not grapheme clusters).
Python: len(str) returns the number of Unicode code points. Python 3 strings are Unicode by default. len("😀") returns 1. For byte count: len("text".encode('utf-8')).
Go: len(str) returns byte length. len([]rune(str)) returns code point count. Use the unicode/utf8 package for accurate counting.
Java/Kotlin: String.length() returns UTF-16 code unit count (same as JavaScript). Use codePointCount() for code points, or a Unicode segmenter library for grapheme clusters.
Practical Recommendations
For displaying a character counter to users, use grapheme cluster count via Intl.Segmenter. This matches what the user perceives as "characters typed."
For database column sizing, measure the maximum expected byte length (not character count) and add a safety margin. Test with your longest expected emoji sequences.
For API payloads, always document whether your character limits refer to code points, grapheme clusters, or bytes. Different clients may count differently.
For SMS sending, check whether your message contains any non-GSM-7 character before sending. A single emoji changes the encoding to UCS-2 and halves the per-segment character limit.
Frequently Asked Questions
Why does "😀".length return 2 in JavaScript?
JavaScript strings are encoded as UTF-16 internally. The 😀 emoji has a Unicode code point of U+1F600, which is above U+FFFF and requires two UTF-16 code units (a surrogate pair). So .length returns 2. To get the code point count, use the spread operator: [..."😀"].length returns 1. For grapheme cluster count, use Intl.Segmenter.
How do I count characters the same way X (Twitter) does?
X uses weighted character counting where most characters are 1, URLs are normalized to 23 characters, and some Unicode ranges count as 2. The exact algorithm is documented in their developer API. For production use, call their text processing API endpoint rather than trying to replicate it locally, as the weighting rules can change.
What is the safest MySQL column type for storing emoji?
Use VARCHAR(n) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci. MySQL's standard utf8 charset only supports 3 bytes per character and cannot store emoji or other characters above U+FFFF. The utf8mb4 charset supports the full Unicode range. Set this at the column, table, or database level to avoid silent data truncation.
Is there a performance difference between counting methods?
str.length in JavaScript is O(1) — it's a stored property. Spreading a string ([...str]) or using Intl.Segmenter is O(n) and involves iteration. For real-time counting as a user types, the difference is negligible for typical text lengths. For very long strings (100,000+ characters), you may want to throttle updates.
Tool
Count characters, words, bytes, and lines in real time with the Character Counter tool. It handles Unicode correctly and shows both character count and UTF-8 byte size simultaneously.