Zeichenlimits, die wirklich wehtun: Code-Einheiten, Codepunkte und Graphemcluster
Veröffentlicht am 1.7.2026 · 10 Min. Lesezeit · Text- & Sprach-Tools
Daniel Okonkwo — Front-end-Entwickler und Tech-Redakteur bei OneKitly
Web-Performance · Dateiformate
Anhand von 7 Quellen geprüft
Das Wort Zeichen benennt drei verschiedene Einheiten, und jedes Limit, an das du stößt, ist in einer davon formuliert, ohne zu sagen in welcher. Eine UTF-16-Code-Einheit ist das, was .length in JavaScript und String.length() in Java zurückgeben. Ein Codepunkt ist ein Unicode-Skalarwert, das Ergebnis von len() in Python 3. Ein Graphemcluster ist das, was Lesende ein Zeichen nennen, das Ergebnis von String.count in Swift. Sie laufen auseinander, sobald der Text reines ASCII verlässt. Das Daumen-hoch-Emoji mit mittlerem Hautton ist 1 Graphemcluster, 2 Codepunkte und 4 UTF-16-Einheiten und belegt 8 Bytes in UTF-8. Das Vier-Personen-Familien-Emoji ist 1 Graphemcluster, 7 Codepunkte, 11 UTF-16-Einheiten und 25 Bytes. Die Zeichenkette Shipping to, Frankreich-Flagge, today, Daumen hoch mit Hautton, Geviertstrich und thanks misst 37 UTF-16-Einheiten, 33 Codepunkte, 31 Graphemcluster und 47 UTF-8-Bytes - vier Zahlen für eine Kette. Plattformen wählen unterschiedliche Einheiten: X zählt eine gewichtete Länge, Bluesky erzwingt 300 Graphemcluster und 3000 Bytes zugleich, Mastodon zählt Codepunkte. Eine SMS fasst 160 Zeichen in GSM-7, aber nur 70 in UCS-2, und ein einziges typografisches Apostroph kippt die ganze Nachricht. Und VARCHAR(255) meint Zeichen in PostgreSQL und MySQL, in Oracle standardmäßig Bytes.
Ein Zeichen ist gleich drei Dinge. Ein Emoji mit Hautton ist 1 Graphemcluster, 2 Codepunkte und 4 UTF-16-Einheiten. Alle Zählungen in diesem Leitfaden wurden in Node gemessen - dazu, warum eine SMS von 160 auf 70 fällt und warum VARCHAR(255) keine 255 von irgendetwas Bestimmtem sind.
Drei Einheiten, alle Zeichen genannt
Eine UTF-16-Code-Einheit sind 16 Bit Speicher. Alles oberhalb von U+FFFF - also jedes Emoji, jede historische Schrift und ein guter Teil von CJK - braucht zwei davon, ein sogenanntes Ersatzzeichenpaar. JavaScript-Strings sind als Folgen von UTF-16-Code-Einheiten definiert, .length zählt also diese - ebenso String.length() in Java und der .NET-String in C#.
Ein Codepunkt ist ein Eintrag in der Unicode-Zeichendatenbank, geschrieben U+0041 oder U+1F44D. Diese Einheit liefert len() in Python 3, und sie verwenden auch der Spread-Operator und die for-of-Iteration in JavaScript. Sie liegt näher an der Intuition als eine Code-Einheit, ist aber immer noch nicht das, was Lesende sehen, denn mehrere Codepunkte verbinden sich regelmäßig zu einem sichtbaren Zeichen.
Ein Graphemcluster - der Standard sagt erweiterter Graphemcluster, definiert in Unicode Annex 29 - ist das vom Menschen wahrgenommene Zeichen. Es ist das, worüber der Cursor springt, was die Rücktaste löscht und was eine Person zählt. Swifts String.count liefert Graphemcluster, ebenso Intl.Segmenter in JavaScript mit der Granularität grapheme. Keine andere verbreitete Sprache liefert sie standardmäßig - das ist die Ursache fast jedes Emoji-Bugs, den du je gesehen hast.
Eine Zeichenkette, vier Zahlen
Nimm eine kurze, völlig alltägliche Nachricht: Shipping to, ein Frankreich-Flaggen-Emoji, today, ein Daumen hoch mit mittlerem Hautton, ein Geviertstrich und thanks mit Ausrufezeichen. In Node 22 gemessen sind das 37 UTF-16-Code-Einheiten, 33 Codepunkte, 31 Graphemcluster und 47 Bytes in UTF-8. Ein Mensch, der sie liest, würde 31 Zeichen sagen. JavaScript sagt dir 37. Eine byte-begrenzte Datenbankspalte sieht 47.
Die Differenzen kommen von zwei Stellen. Das Flaggen-Emoji ist ein Paar regionaler Indikatorsymbole - zwei Codepunkte, vier UTF-16-Einheiten, eine sichtbare Flagge - und der Daumen hoch ist ein Basis-Emoji plus Hautton-Modifikator, wieder zwei Codepunkte und vier UTF-16-Einheiten für ein sichtbares Zeichen. Zusammen erklären sie 37 gegenüber 31. Nichts Exotisches: So sieht eine normale Nachricht von einem normalen Telefon aus.
Derselbe sichtbare Text kann zwei verschiedene Längen haben
Das Wort Cafe mit Akut, gefolgt von einem Leerzeichen, einem hautgetönten Daumen hoch und einem Ausrufezeichen, misst 10 UTF-16-Einheiten, 8 Codepunkte, 7 Graphemcluster und 15 UTF-8-Bytes, wenn der Akzentbuchstabe der vorkomponierte Codepunkt U+00E9 ist. Tippe dieselbe aussehende Kette auf einem Mac, der stattdessen e plus kombinierenden Akut liefert, und daraus werden 11 UTF-16-Einheiten, 9 Codepunkte, weiterhin 7 Graphemcluster und 16 UTF-8-Bytes.
Gleiche Pixel, andere Länge - und eine Byte-Prüfung, die im einen Fall besteht, scheitert im anderen. Die Lösung ist Normalisierung beim Eingang: Wende die Unicode-Normalisierungsform C an, die e plus kombinierenden Akut wieder zu U+00E9 zusammenzieht, bevor du misst, speicherst, vergleichst oder hashst. Fast jeder Dublettenfehler mit Akzentnamen geht darauf zurück, dass eine NFC-Kette mit einer NFD-Kette verglichen wurde.
Beim Kürzen bricht die Abstraktion vor Publikum
Schneide diese Cafe-Kette mit einem einfachen slice bei sechs UTF-16-Einheiten ab, und du bekommst C, a, f, das Akzent-e, ein Leerzeichen und dann U+D83D allein - die obere Hälfte eines Ersatzzeichenpaars ohne Gegenstück. Dieses einsame Surrogat ist kein gültiges Zeichen. Renderer zeigen ein Ersatzkästchen, JSON-Encoder erzeugen eine Escape-Folge, die manche Parser ablehnen, und Datenbanken mit strenger UTF-8-Prüfung verweigern das Schreiben schlicht.
Nach Codepunkten zu kürzen vermeidet den Absturz durch das einsame Surrogat, zerschneidet aber weiterhin ein Familien-Emoji in zwei Erwachsene und ein Kind oder streift den Hautton vom Daumen hoch, der dann gelb erscheint. Das einzig sichere Kürzen vor einem Nutzer ist graphembasiert: mit Intl.Segmenter segmentieren, die ersten n Segmente nehmen, zusammenfügen. Ist dein Limit in Bytes formuliert, mach beides - Graphemcluster zählen, um zu entscheiden, wo ein Schnitt zulässig ist, und UTF-8-Bytes, um zu entscheiden, wie viele du dir leisten kannst.
Welche Plattform welche Einheit zählt
X zählt gar keine Zeichen, sondern berechnet eine gewichtete Länge: Zeichen aus den Bereichen Latin, Latin-1-Ergänzung und allgemeine Interpunktion zählen 1, alles andere - CJK, Arabisch, Kyrillisch jenseits des Grundblocks und jedes Emoji - zählt 2. Ein Beitrag mit Limit 280 fasst also 280 lateinische Buchstaben oder 140 Emojis. Bluesky erzwingt zwei Limits gleichzeitig auf demselben Feld: 300 Graphemcluster und 3.000 Bytes; ein Beitrag aus 300 Flaggen-Emojis besteht die Graphemprüfung und scheitert an den Bytes. Mastodons Standard von 500 zählt Codepunkte, wobei jede URL pauschal mit 23 berechnet wird, egal wie lang sie wirklich ist.
Praktisch heißt das: Ein einziges Zählwerkzeug kann nicht alle Ziele bedienen. Wer denselben Text für vier Netzwerke plant, braucht vier Zählungen und eine Vorschau, die die Kette so zeigt, wie sie gelesen wird, nicht wie die Speicherschicht sie misst. Im Zweifel zähle Graphemcluster für das menschenseitige Limit und UTF-8-Bytes für das maschinenseitige - und misstraue jeder einzelnen Zahl mit dem Etikett Zeichen, solange du nicht weißt, welche Einheit sie erzeugt hat.
SMS: Aus 160 werden 70, sobald ein Zeichen GSM-7 verlässt
Ein SMS-Rumpf hat 140 Oktette. Kodiert im GSM-7-Bit-Standardalphabet aus 3GPP TS 23.038, sieben Bit pro Zeichen, ergibt das 160 Zeichen. Dieses Alphabet hat aber nur rund 128 Plätze plus eine kleine Erweiterungstabelle, und sobald auch nur ein Zeichen der Nachricht darin fehlt, muss die gesamte Nachricht in UCS-2 mit 16 Bit pro Zeichen neu kodiert werden: 140 Oktette geteilt durch 2 sind 70 Zeichen. Nicht 70 für das störende Zeichen - 70 für die ganze Nachricht.
Ein Beispiel. Eine Buchungsbestätigung mit 145 Zeichen passt in ein einziges GSM-7-Segment, denn 145 liegt unter 160. Füge sie aus einer Textverarbeitung ein, die den geraden Apostroph stillschweigend in einen typografischen verwandelt hat, oder ergänze einen Geviertstrich oder ein Emoji - schon wird die Nachricht UCS-2. Nun liegen 145 über 70, also wird sie in verkettete Teile zerlegt, und die Verkettung zweigt pro Teil sechs Oktette für den Segmentierungskopf ab, sodass je 67 Zeichen bleiben. 145 geteilt durch 67 rundet auf 3 auf. Eine unsichtbare Ersetzung machte aus einer abrechenbaren Nachricht drei.
Welche Zeichen sicher sind, ist nicht intuitiv. Mehrere Akzentbuchstaben stehen im GSM-7-Grundsatz - e mit Akut, e mit Gravis, a mit Gravis, u mit Gravis, das scharfe s und die Umlautvokale gehören dazu -, eine französische oder deutsche SMS ist also nicht automatisch UCS-2. Aber a, i, o und u mit Akut fehlen, weshalb spanische und portugiesische Akzente den Wechsel meist erzwingen. Das Eurozeichen liegt in der Erweiterungstabelle und kostet zwei statt einem Septett. Typografische Anführungszeichen, Halbgeviert- und Geviertstriche, das Auslassungszeichen und sämtliche Emojis fehlen schlicht.
VARCHAR(255) sind keine 255 von irgendetwas Bestimmtem
In PostgreSQL sind varchar(255) 255 Zeichen, also Codepunkte, und der Speicher wächst auf das, was UTF-8 braucht. In MySQL zählt VARCHAR(255) seit Version 5.0 Zeichen statt Bytes, unter dem Zeichensatz utf8mb4 kann dieselbe Spalte also bis zu 1.020 Bytes belegen. In Oracle bedeutet VARCHAR2(255) 255 Bytes, sofern du nicht VARCHAR2(255 CHAR) schreibst oder NLS_LENGTH_SEMANTICS änderst - deshalb weist ein 255er-Feld in einem Oracle-System einen 200 Zeichen langen Namen in einem Akzentalphabet stillschweigend zurück. In SQL Server sind VARCHAR Bytes bei einer Einbyte-Sortierung und NVARCHAR UTF-16-Einheiten zu je zwei Bytes.
Es gibt eine Falle zweiter Ordnung speziell in MySQL. Das alte InnoDB-Indexpräfixlimit von 767 Bytes bedeutete, dass eine VARCHAR(255)-Spalte in utf8mb4 - bis zu 1.020 Bytes - nicht vollständig indizierbar war; daher stammt die Folklore vom VARCHAR(191), denn 191 mal 4 sind 764, knapp unter dem Limit. Modernes InnoDB mit dem Zeilenformat DYNAMIC hebt das Präfixlimit auf 3.072 Bytes, der Behelf ist überholt - doch die 191er-Spalten, die er hervorbrachte, stecken bis heute überall in Produktivschemata.
| Zeichenkette | UTF-16-Einheiten (.length) | Codepunkte | Graphemcluster | UTF-8-Bytes |
|---|---|---|---|---|
| Der Buchstabe a | 1 | 1 | 1 | 1 |
| e mit Akut, als ein Codepunkt U+00E9 geschrieben | 1 | 1 | 1 | 2 |
| Derselbe Buchstabe als e plus kombinierender Akut U+0065 U+0301 | 2 | 2 | 1 | 3 |
| Daumen-hoch-Emoji mit mittlerem Hautton (U+1F44D U+1F3FD) | 4 | 2 | 1 | 8 |
| Familien-Emoji: Mann, Frau, Mädchen, Junge, verbunden durch drei Zero-Width-Joiner | 11 | 7 | 1 | 25 |
Häufige Fragen
- Warum sagt JavaScript, mein Emoji sei 2 Zeichen lang?
- Weil .length UTF-16-Code-Einheiten zählt und jedes Emoji oberhalb von U+FFFF liegt, also ein Ersatzzeichenpaar braucht - zwei Einheiten. Kommt ein Hautton-Modifikator dazu, sind es vier. Nimm [...str].length für Codepunkte oder Intl.Segmenter mit der Granularität grapheme, um zu zählen, was Lesende zählen würden. Als Einzeiler: new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(str) liefert dir ein iterierbares Objekt sichtbarer Zeichen.
- Wie viele Bytes belegt ein Zeichen in UTF-8?
- Eins bis vier, je nach Codepunkt. ASCII bis U+007F belegt 1 Byte. Lateinische Akzente, Griechisch und Kyrillisch bis U+07FF belegen 2. Der Großteil der Basic Multilingual Plane, CJK eingeschlossen, belegt 3. Alles oberhalb von U+FFFF, also sämtliche Emojis, belegt 4. Ein hautgetönter Daumen hoch sind daher 8 Bytes, denn er besteht aus zwei Codepunkten zu je 4 Bytes, und das Vier-Personen-Familien-Emoji sind 25 Bytes: vier Emojis zu 4 Bytes plus drei Zero-Width-Joiner zu je 3 Bytes.
- Warum wurde meine SMS mit 145 Zeichen als drei Nachrichten abgerechnet?
- Weil ein Zeichen darin nicht im GSM-7-Bit-Alphabet stand, wechselte die ganze Nachricht auf UCS-2 mit 70 Zeichen pro Segment. Über 70 tragen die Segmente einen Verkettungskopf, der sechs Oktette kostet, sodass je 67 Zeichen bleiben - und 145 geteilt durch 67 rundet auf 3 auf. Die üblichen Verdächtigen sind ein typografisches Apostroph, ein Geviertstrich, ein Auslassungszeichen oder ein Emoji, die Textverarbeitungen und Handytastaturen automatisch einsetzen. Schick den Text vor einer Kampagne durch einen Byte- und Kodierungszähler, nicht danach.
- Soll ich ein Limit in Zeichen oder in Bytes speichern?
- Erzwinge zwei Limits, nicht eines. Ein Graphemlimit für das, was du dem Nutzer zeigst, denn diese Zahl kann er mit bloßem Auge prüfen. Ein Bytelimit für Speicherung und Transport, denn genau das beschränken Spalte, Nutzlast und Protokoll wirklich. Wenn nur eines möglich ist, nimm Bytes und mach die Oberfläche darüber ehrlich: Ein einziges Byte zu viel ist ein Schreibfehler, ein Graphem zu viel nur ein Schönheitsproblem.
- Gelten dieselben Regeln für Meta-Description und Title-Tag?
- Nein, und genau hier ist Zeichenzählen der falsche Reflex. Suchmaschinen kürzen Titel und Beschreibungen nach gerenderter Pixelbreite, nicht nach Zeichenzahl: Ein Titel aus 60 schmalen Buchstaben passt, wo 50 breite nicht passen, und ein Emoji im Titel belegt weit mehr Breite, als sein einzelner Graphemcluster vermuten lässt. Zielwerte wie 60 und 155 Zeichen sind Faustregeln, die ein Pixelbudget annähern. Schreib für das Pixelbudget, prüfe in einer Suchergebnis-Vorschau und nutze den Zeichenzähler nur, um nicht in offensichtlich unrettbare Längen abzudriften.
Artikel, die dich interessieren könnten
Alle Ratgeber →Ähnliche Tools
Quellen
- Unicode Consortium — UAX #29: Unicode Text Segmentation (grapheme cluster boundaries)
- Unicode Consortium — UAX #15: Unicode Normalization Forms (NFC, NFD, NFKC, NFKD)
- Unicode Consortium — UTS #51: Unicode Emoji (emoji modifiers and ZWJ sequences)
- Ecma International — ECMAScript Language Specification: String values are sequences of 16-bit code units
- 3GPP — TS 23.038: Alphabets and language-specific information (GSM 7-bit default alphabet)
- Oracle / MySQL — MySQL Reference Manual: The CHAR and VARCHAR Types
- PostgreSQL Global Development Group — PostgreSQL Documentation: Character Types
Hast du einen Fehler in diesem Artikel entdeckt?