¿Qué formato usar para las marcas de tiempo en JSON?
En resumen: Los números epoch y las cadenas ISO tienen cada uno sus inconvenientes, pero mezclarlos es lo que realmente rompe las cosas. Usa ISO 8601 con desplazamiento, almacena en UTC y muestra en la zona horaria del usuario.

En resumen: usa ISO 8601 con un desplazamiento explícito en el cable, almacena UTC con una Z final y convierte solo al mostrar; si en cambio eliges números epoch, fija segundos frente a milisegundos de forma global y pon la unidad en el nombre del campo.
Por qué 2026-10-30 14:30:00 no basta
Esa cadena no lleva zona. Las 14:30 en Shanghái y las 14:30 en Londres están a ocho horas de distancia, y no hay forma de saber cuál se quiere decir. new Date('2026-10-30 14:30:00') analiza en la zona local del runtime, así que el servidor y el navegador derivan dos instantes distintos de una misma carga útil: el tipo de error que nunca se reproduce.
Un 2026-10-30 desnudo no es un instante en absoluto. Está bien para cumpleaños y periodos de facturación, mal para una hora de creación.
Dos formatos viables
Cadenas ISO 8601
{
"createdAt": "2026-10-30T14:30:00+08:00",
"updatedAt": "2026-10-30T06:30:00Z"
}
Legibles, ordenables cuando el formato es coherente, con precisión de milisegundos si hace falta, y autodescriptivas.
Números epoch
{ "createdAt": 1793519400 }
Compactos e inequívocos, pero ilegibles, y segundos frente a milisegundos es un peligro permanente. Si eliges epoch, estandariza la unidad y ponla en el nombre del campo: createdAtMs es mucho más seguro que createdAt.
Convenciones que conviene fijar pronto
| Elemento | Convención |
|---|---|
| Formato en el cable | ISO 8601 con Z o +08:00 |
| Zona de almacenamiento | UTC, para que el horario de verano nunca entre en escena |
| Precisión | Los milisegundos bastan |
| Nombres de campo | createdAt, updatedAt, expiresAt |
| Fechas puras | Un campo date separado, p. ej. birthday: "1990-01-01" |
| Duraciones | Entero más una unidad, p. ej. timeoutMs: 3000 |
Renderizado
new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Europe/London',
}).format(new Date('2026-10-30T06:30:00Z'));
Especifica timeZone explícitamente cuando la visualización no deba seguir al dispositivo. Confiar en el valor predeterminado del navegador es correcto para horas orientadas al usuario y erróneo para informes.
Tres errores recurrentes
Las fechas desnudas se analizan como medianoche UTC. Según la especificación ECMAScript, las cadenas solo de fecha son UTC mientras que las cadenas de fecha y hora son locales. Esa inconsistencia es una fuente fiable de errores de un día de diferencia.
La zona de la base de datos no coincide con el serializador. El datetime de MySQL no tiene zona; escribe local, lee como UTC, y todo se desplaza por el offset.
Horario de verano. Almacenar horas locales futuras crea instantes que no existen o ocurren dos veces. UTC no tiene esos huecos.
FAQ
¿Cómo debo elegir entre ISO 8601 y números epoch?
Usa ISO 8601 cuando las personas leen la carga útil, cuando quieres orden lexicográfico o cuando el valor debe llevar su propia zona; el coste es aproximadamente una docena de bytes extra y una biblioteca de análisis. Usa números epoch cuando la compacidad y el manejo trivial entre lenguajes importen más, pero fija segundos frente a milisegundos de forma global y di qué unidad guarda el campo.
¿Deberían una fecha desnuda y una marca de tiempo compartir el mismo campo?
No. Un 2026-10-30 desnudo es un día de calendario sin hora y sin zona, mientras que se espera que un campo de marca de tiempo lleve un instante exacto. Mantén cumpleaños y periodos de facturación en un campo date separado, y usa un campo ISO 8601 At para horas de creación o actualización.