Los límites de caracteres que de verdad muerden: unidades de código, puntos de código y grafemas
Publicado el 1/7/2026 · 12 min de lectura · Herramientas de texto e idioma
Daniel Okonkwo — Desarrollador front-end y redactor de Tecnología en OneKitly
Rendimiento web · Formatos de archivo
Verificado con 7 fuentes
La palabra carácter nombra tres unidades distintas, y cada límite con el que chocas está expresado en una de ellas sin decir cuál. Una unidad de código UTF-16 es lo que devuelven .length en JavaScript y String.length() en Java. Un punto de código es un valor escalar Unicode, lo que devuelve len() en Python 3. Un grafema es lo que un lector llama carácter, lo que devuelve String.count en Swift. Divergen en cuanto el texto abandona el ASCII puro. El emoji del pulgar arriba con tono de piel medio es 1 grafema, 2 puntos de código y 4 unidades UTF-16, y ocupa 8 bytes en UTF-8. El emoji de familia de cuatro es 1 grafema, 7 puntos de código, 11 unidades UTF-16 y 25 bytes. La cadena Shipping to, bandera francesa, today, pulgar arriba con tono, raya y thanks mide 37 unidades UTF-16, 33 puntos de código, 31 grafemas y 47 bytes UTF-8: cuatro números para una sola cadena. Las plataformas eligen unidades distintas: X cuenta una longitud ponderada, Bluesky impone 300 grafemas y 3000 bytes a la vez, Mastodon cuenta puntos de código. Un SMS lleva 160 caracteres en GSM-7 pero solo 70 en UCS-2, y una comilla tipográfica convierte el mensaje entero. Y VARCHAR(255) significa caracteres en PostgreSQL y MySQL, y bytes en Oracle por defecto.
Un carácter son tres cosas a la vez. Un emoji con tono de piel es 1 grafema, 2 puntos de código y 4 unidades UTF-16. Todos los recuentos de esta guía se midieron en Node, más por qué un SMS cae de 160 a 70 y por qué VARCHAR(255) no son 255 de nada en concreto.
Tres unidades, todas llamadas carácter
Una unidad de código UTF-16 son 16 bits de almacenamiento. Todo lo que supera U+FFFF - es decir, todos los emojis, todas las escrituras históricas y buena parte del CJK - necesita dos, formando un par sustituto. Las cadenas de JavaScript se definen como secuencias de unidades de código UTF-16, así que .length cuenta esas, igual que String.length() en Java y el tipo string de .NET en C#.
Un punto de código es una entrada de la base de caracteres Unicode, escrita U+0041 o U+1F44D. Es la unidad que devuelve len() en Python 3 y la que usan el operador de propagación y la iteración for-of en JavaScript. Está más cerca de la intuición que una unidad de código, pero sigue sin ser lo que ve un lector, porque varios puntos de código se combinan rutinariamente en un solo signo visible.
Un grafema - la norma lo llama grafema extendido, definido en el anexo 29 de Unicode - es el carácter percibido por el usuario. Es lo que salta el cursor, lo que borra un retroceso y lo que una persona cuenta. String.count en Swift devuelve grafemas; también Intl.Segmenter en JavaScript con granularidad grapheme. Ningún otro lenguaje mayoritario los devuelve por defecto, y esa es la causa raíz de casi todos los fallos con emojis que has visto.
Una cadena, cuatro números
Toma un mensaje corto y del todo corriente: Shipping to, un emoji de bandera francesa, today, un pulgar arriba con tono de piel medio, una raya y thanks con signo de exclamación. Medido en Node 22 son 37 unidades de código UTF-16, 33 puntos de código, 31 grafemas y 47 bytes en UTF-8. Una persona que lo lea diría que tiene 31 caracteres. JavaScript te dirá 37. Una columna de base de datos limitada en bytes verá 47.
Las diferencias vienen de dos sitios. El emoji de bandera es un par de indicadores regionales - dos puntos de código, cuatro unidades UTF-16, una sola bandera visible - y el pulgar arriba es un emoji base más un modificador de tono, de nuevo dos puntos de código y cuatro unidades UTF-16 para un solo signo visible. Entre ambos explican el 37 frente al 31. No pasa nada exótico: así es un mensaje normal enviado desde un teléfono normal.
El mismo texto visible puede tener dos longitudes distintas
La palabra Cafe con acento agudo, seguida de un espacio, un pulgar arriba con tono y un signo de exclamación, mide 10 unidades UTF-16, 8 puntos de código, 7 grafemas y 15 bytes UTF-8 cuando la letra acentuada es el punto de código precompuesto U+00E9. Teclea la cadena de aspecto idéntico en un Mac que emite una e seguida de un acento agudo combinante y pasa a 11 unidades UTF-16, 9 puntos de código, todavía 7 grafemas, y 16 bytes UTF-8.
Los mismos píxeles, distinta longitud, y una comprobación de bytes que pasa en un caso falla en el otro. La solución es normalizar en la entrada: aplica la forma de normalización Unicode C, que recompone e más acento combinante en U+00E9, antes de medir, almacenar, comparar o calcular un hash. Casi todos los fallos de detección de duplicados con nombres acentuados se remontan a comparar una cadena NFC con una NFD.
El truncado es donde la abstracción se rompe en público
Corta esa cadena Cafe a seis unidades UTF-16 con un slice normal y obtienes C, a, f, la e acentuada, un espacio y luego U+D83D a solas: la mitad alta de un par sustituto sin pareja. Ese sustituto suelto no es un carácter válido. Los renderizadores muestran una caja de reemplazo, los codificadores JSON emiten un escape que algunos analizadores rechazan y las bases de datos con validación UTF-8 estricta rechazan la escritura sin más.
Truncar por puntos de código evita el fallo del sustituto suelto, pero sigue partiendo un emoji de familia en dos adultos y un niño, o quitando el modificador de tono a un pulgar arriba que vuelve a salir amarillo. El único truncado seguro delante de un usuario es el basado en grafemas: segmenta con Intl.Segmenter, toma los primeros n segmentos y únelos. Si tu límite está en bytes, haz las dos cosas: cuenta grafemas para decidir dónde es legal cortar, y bytes UTF-8 para decidir cuántos te puedes permitir.
Qué plataforma cuenta qué unidad
X no cuenta caracteres en absoluto: calcula una longitud ponderada en la que los caracteres de los rangos latino, suplemento latino-1 y puntuación general cuentan 1, y todo lo demás - CJK, árabe, cirílico más allá del bloque básico y cada emoji - cuenta 2. Un post con límite de 280 admite por tanto 280 letras latinas o 140 emojis. Bluesky impone dos límites a la vez sobre el mismo campo: 300 grafemas y 3000 bytes, así que un post de 300 emojis de bandera es legal en grafemas y falla en bytes. El límite por defecto de 500 en Mastodon se cuenta en puntos de código, y cualquier URL se cobra a 23 fijos sea cual sea su longitud real.
La consecuencia práctica es que un único contador no puede servir a todos los destinos. Si programas el mismo texto en cuatro redes, necesitas cuatro recuentos y una vista previa que muestre la cadena tal como la verá el lector, no como la mide la capa de almacenamiento. En la duda, cuenta grafemas para el límite de cara al humano y bytes UTF-8 para el de cara a la máquina, y desconfía de cualquier cifra única etiquetada como caracteres hasta saber qué unidad la produjo.
SMS: 160 se convierte en 70 en cuanto un carácter sale del GSM-7
El cuerpo de un SMS son 140 octetos. Codificado en el alfabeto GSM de 7 bits definido en la norma 3GPP TS 23.038, a siete bits por carácter, eso da 160 caracteres. Pero ese alfabeto tiene solo unos 128 huecos más una pequeña tabla de extensión, y si un solo carácter del mensaje queda fuera, todo el mensaje debe recodificarse en UCS-2 a 16 bits por carácter: 140 octetos entre 2 son 70 caracteres. No 70 para el carácter culpable: 70 para el mensaje entero.
Veamos un ejemplo. Una confirmación de reserva de 145 caracteres cabe en un solo segmento GSM-7, porque 145 es menos de 160. Pégala desde un procesador de textos que ha convertido en silencio el apóstrofo recto en uno tipográfico, o añade una raya, o un emoji: el mensaje pasa a UCS-2. Ahora 145 supera 70, así que se parte en partes concatenadas, y la concatenación roba seis octetos por parte para la cabecera de segmentación, dejando 67 caracteres en cada una. 145 dividido entre 67 se redondea a 3. Una sustitución invisible convirtió un mensaje facturable en tres.
Qué caracteres son seguros no es intuitivo. Varias letras acentuadas sí están en el juego básico GSM-7 - e con acento agudo, e grave, a grave, u grave, la ese alemana y las vocales con diéresis entre ellas -, así que un SMS francés o alemán no es automáticamente UCS-2. Pero la a, la i, la o y la u con acento agudo no están, lo que significa que los acentos españoles y portugueses suelen forzar el cambio. El signo del euro vive en la tabla de extensión y cuesta dos septetos en lugar de uno. Las comillas tipográficas, las rayas y semirrayas, el carácter de puntos suspensivos y todos los emojis simplemente no existen ahí.
VARCHAR(255) no son 255 de nada en concreto
En PostgreSQL, varchar(255) son 255 caracteres, es decir puntos de código, y el almacenamiento crece hasta lo que UTF-8 necesite. En MySQL, VARCHAR(255) cuenta caracteres y no bytes desde la versión 5.0, así que con el juego utf8mb4 la misma columna puede ocupar hasta 1020 bytes. En Oracle, VARCHAR2(255) significa 255 bytes salvo que escribas VARCHAR2(255 CHAR) o cambies NLS_LENGTH_SEMANTICS, y por eso un campo con límite 255 en un sistema con Oracle rechaza en silencio un nombre de 200 caracteres escrito en un alfabeto acentuado. En SQL Server, VARCHAR son bytes en una intercalación de un byte y NVARCHAR son unidades UTF-16 de dos bytes cada una.
Hay una trampa de segundo orden específica de MySQL. El antiguo límite de prefijo de índice de InnoDB de 767 bytes hacía que una columna VARCHAR(255) en utf8mb4 - hasta 1020 bytes - no pudiera indexarse por completo, y de ahí viene el folclore del VARCHAR(191): 191 por 4 son 764, justo por debajo del límite. El InnoDB moderno con formato de fila DYNAMIC eleva ese límite a 3072 bytes y el apaño quedó obsoleto, pero las columnas de longitud 191 que creó siguen en esquemas de producción por todas partes.
| Cadena | Unidades UTF-16 (.length) | Puntos de código | Grafemas | Bytes UTF-8 |
|---|---|---|---|---|
| La letra a | 1 | 1 | 1 | 1 |
| e con acento agudo, escrita como un único punto de código U+00E9 | 1 | 1 | 1 | 2 |
| La misma letra tecleada como e más un acento combinante U+0065 U+0301 | 2 | 2 | 1 | 3 |
| Emoji de pulgar arriba con tono de piel medio (U+1F44D U+1F3FD) | 4 | 2 | 1 | 8 |
| Emoji de familia: hombre, mujer, niña, niño unidos por tres uniones de ancho cero | 11 | 7 | 1 | 25 |
Preguntas frecuentes
- ¿Por qué JavaScript dice que mi emoji mide 2 caracteres?
- Porque .length cuenta unidades de código UTF-16, y todo emoji está por encima de U+FFFF, así que necesita un par sustituto: dos unidades. Añade un modificador de tono y son cuatro. Usa [...str].length para contar puntos de código, o Intl.Segmenter con granularidad grapheme para contar lo que contaría un lector. En una línea: new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(str) te da un iterable de caracteres visibles.
- ¿Cuántos bytes ocupa un carácter en UTF-8?
- De uno a cuatro, según el punto de código. El ASCII hasta U+007F ocupa 1 byte. Los acentos latinos, el griego y el cirílico hasta U+07FF ocupan 2. Casi todo el plano multilingüe básico, incluido el CJK, ocupa 3. Todo lo que supera U+FFFF, es decir todos los emojis, ocupa 4. Así que un pulgar arriba con tono son 8 bytes, porque son dos puntos de código de 4 bytes cada uno, y el emoji de familia de cuatro son 25 bytes: cuatro emojis a 4 bytes más tres uniones de ancho cero a 3 bytes cada una.
- ¿Por qué mi SMS de 145 caracteres se facturó como tres mensajes?
- Porque uno de sus caracteres no estaba en el alfabeto GSM de 7 bits, así que todo el mensaje pasó a UCS-2, a 70 caracteres por segmento. Por encima de 70, los segmentos llevan una cabecera de concatenación que cuesta seis octetos, dejando 67 caracteres en cada uno, y 145 entre 67 se redondea a 3. Los culpables habituales son un apóstrofo tipográfico, una raya, un carácter de puntos suspensivos o un emoji, todos ellos insertados automáticamente por procesadores de texto y teclados de móvil. Pasa el texto por un contador de bytes y codificación antes de enviar una campaña, no después.
- ¿Debo guardar un límite en caracteres o en bytes?
- Aplica dos límites, no uno. Un límite en grafemas para lo que le muestras al usuario, porque es la cifra que puede verificar mirando. Un límite en bytes para almacenamiento y transporte, porque es lo que realmente restringen la columna, la carga útil y el protocolo. Si solo puedes tener uno, que sean bytes y que la interfaz sea honesta al respecto: un desbordamiento de un solo byte es un fallo de escritura, mientras que uno de grafemas es solo un problema estético.
- ¿Se aplican las mismas reglas a una metadescripción o etiqueta title?
- No, y este es justo el caso donde contar caracteres es el instinto equivocado. Los buscadores truncan títulos y descripciones por ancho en píxeles renderizado, no por número de caracteres, así que un título de 60 letras estrechas puede caber donde 50 anchas no caben, y un emoji en un título ocupa mucho más ancho de lo que sugiere su único grafema. Los objetivos de 60 y 155 caracteres son aproximaciones a un presupuesto de píxeles. Escribe para el presupuesto de píxeles, verifica en una vista previa de resultados y usa el contador de caracteres solo para no derivar hacia longitudes claramente irrecuperables.
Artículos que podrían interesarte
Todas las guías →Herramientas relacionadas
Fuentes
- 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
¿Has detectado un error en este artículo?