Saltar al contenido principal
Tutorial

Timestamps Unix en producción: segundos vs milisegundos, el problema 2038 y bugs de zona horaria

· 12 min de lectura

Todo sistema de producción tarde o temprano registra, almacena o transmite un timestamp Unix. Y todo sistema de producción tarde o temprano lanza un bug que involucra uno — una fecha que se muestra como enero de 1970, un evento programado 49 años en el pasado, una integración de API donde un lado espera milisegundos y el otro envía segundos. Estos bugs son aburridos, comunes y completamente evitables una vez conoces las pocas reglas que gobiernan cómo funcionan realmente los timestamps.

Esta guía cubre el núcleo práctico: reconocer las unidades del timestamp por el conteo de dígitos, el problema 2038 y su estado real hoy, los patrones de bugs que realmente ocurren, y la forma correcta de obtener un timestamp en los lenguajes que probablemente estés usando.

Qué es (y qué no es) un timestamp Unix

Un timestamp Unix cuenta el número de segundos desde 1970-01-01 00:00:00 UTC — la «época Unix». Dos propiedades importan para todo lo que sigue:

  1. No tiene zona horaria. El timestamp 1785235837 se refiere a un instante específico simultáneamente en toda la Tierra. Las zonas horarias solo entran cuando muestras un timestamp a un humano o construyes uno a partir de componentes de hora local. Si te quedas con una frase de este artículo, que sea esta: los timestamps son instantes UTC; las zonas horarias son una cuestión de renderizado.
  2. Ignora los segundos intercalares. El tiempo Unix asume que cada día tiene exactamente 86.400 segundos. Cuando se inserta un segundo intercalar, el tiempo POSIX efectivamente lo repite o lo difumina (el comportamiento varía según el sistema; el «leap smear» de Google lo distribuye a lo largo del día). Para la mayoría del código de aplicación esto es irrelevante; para cálculos de intervalos que abarcan un segundo intercalar significa que tu suposición de «exactamente N segundos» puede fallar por uno.

Observa también que la elección de la época significa que los timestamps negativos son válidos: -1 es 1969-12-31 23:59:59 UTC. El código que trata los timestamps negativos como errores se rompe con fechas anteriores a 1970 — un patrón real de bug para sistemas que manejan datos históricos (fechas de nacimiento, archivos).

Segundos, milisegundos, microsegundos: la heurística del conteo de dígitos

Los timestamps llegan en diferentes unidades desde diferentes ecosistemas, y malidentificar la unidad es el bug más común de timestamps. La prueba rápida de campo es contar los dígitos:

UnidadEjemplo (mediados de 2026)DígitosCuándo alcanza este ancho
Segundos17852358371010 dígitos desde 2001-09-09; 11 dígitos desde 2286
Milisegundos17852358370001313 dígitos desde 2001-09-09
Microsegundos17852358370000001616 dígitos
Nanosegundos178523583700000000019excede el rango de int64 en 2262

Reglas prácticas para la era actual (2001–2286):

  • 10 dígitos → segundos. Siempre seguro asumirlo.
  • 13 dígitos → milisegundos. El Date.now() de JavaScript, el System.currentTimeMillis() de Java y la mayoría de sistemas de logging/métricas los emiten.
  • 16 dígitos → microsegundos. El time.time() de Python multiplicado, algunas bases de datos, algunos sistemas de tracing.
  • 19 dígitos → nanosegundos. El time.Now().UnixNano() de Go, y este es una trampa: 19 dígitos excede el máximo de 64 bits con signo (9.223.372.036.854.775.807) después de 2262, pero hoy un número de 19 dígitos cabe en int64 — apenas. Si almacenas timestamps en nanosegundos, no hagas aritmética que asuma espacio.

Una verificación rápida de cordura al depurar: pega el número en un Conversor de Timestamps y mira si la fecha parece sensata. Si una interpretación en «segundos» da 1970 y una en «milisegundos» da una fecha reciente plausible, has identificado la unidad en dos segundos.

El patrón de bug correspondiente: tratar milisegundos como segundos (o viceversa). Multiplica un timestamp en segundos por 1000 sin necesidad y 1785235837 se convierte en 1785235837000 segundos — una fecha aproximadamente 56.000 años en el futuro, que la mayoría de renderizadores recortan o envuelven en basura. Divide un timestamp en milisegundos por 1000 cuando ya era segundos y obtienes 1970-01-21 — por eso tantos sistemas rotos muestran fechas en enero de 1970. Cuando veas 1970 en producción, sospecha primero de un desajuste de unidades.

El problema 2038

El time_t Unix clásico era un entero de 32 bits con signo contando segundos. Su valor máximo, 2.147.483.647, corresponde a 2038-01-19 03:14:07 UTC. Un segundo después, un contador de 32 bits con signo desborda a −2.147.483.648, que se renderiza como 1901-12-13 20:45:52 UTC. Este es el problema «Y2038» o «Epochalypse» — estructuralmente idéntico al Y2K, pero arraigado en un tipo de C específico en lugar de en campos de año de dos dígitos.

Dónde sigue importando realmente, a 2026:

  • Sistemas embebidos y firmware de 32 bits. Dispositivos con largos ciclos de vida — controladores industriales, sistemas automotrices, dispositivos médicos, infraestructura — ejecutando kernels de 32 bits o ABIs con time_t de 32 bits. Esta es la clase de riesgo residual principal, precisamente porque estos sistemas son difíciles de parchear y fáciles de olvidar.
  • Formatos de archivo y protocolos antiguos que especificaban contadores de 32 bits en segundos. Algunos se arreglan reinterpretando el campo como sin signo (empujando el límite a 2106); otros son simplemente legado.
  • Bases de datos con tipos limitados por la época. El ejemplo conocido: el tipo TIMESTAMP de MySQL históricamente solo llegaba hasta 2038-01-19 03:14:07 UTC porque almacenaba segundos como un entero sin signo de 32 bits. (DATETIME tiene rango hasta el año 9999 y no se ve afectado.) Si tu esquema usa TIMESTAMP, conoce este límite.

Qué ya se ha arreglado:

  • Linux de 64 bits y sistemas modernos de 64 bits usan un time_t de 64 bits, válido por ~292 mil millones de años.
  • El trabajo time64 del kernel Linux (completado en el kernel 5.6, 2020) y el soporte de tiempo de 64 bits de glibc (_TIME_BITS=64 de glibc 2.34) dan al userland de Linux de 32 bits un camino de migración, aunque los binarios deben reconstruirse con la nueva ABI — los binarios compilados viejos en sistemas de 32 bits siguen siendo vulnerables.
  • JavaScript, Python, Go, Java usan representaciones de tiempo de 64 bits (o float64) y no desbordan en ningún plazo relevante para humanos. Los milisegundos en float64 de JavaScript llegan a ~285.000 años en cualquier dirección.

Consejo práctico: si estás escribiendo código nuevo en una plataforma de 64 bits con un lenguaje mainstream, 2038 no es tu problema. Si mantienes firmware embebido, C legado en objetivos de 32 bits, o esquemas llenos de columnas de época INT y campos MySQL TIMESTAMP, audita ahora — los sistemas instalados en la década de 2020 seguirán ejecutándose en 2038.

Los patrones de bug que realmente ocurren

1. Confusión de unidades (segundos vs milisegundos)

Cubierto arriba: el más común y el más fácil de detectar (fechas en 1970 o el año 58000). Arréglalo por convención: nombra tus variables y campos con la unidad — created_at_ms, expires_s — y valida en los límites de la API. Un timestamp fuera de, digamos, 10^910^10 segundos es sospechoso para un campo de «segundos» en este siglo.

2. Hora local confundida con UTC

Lo inverso de la regla «los timestamps no tienen zona horaria». Manifestación clásica: un servidor en Shanghái y un servidor en Virginia calculan «el mismo» plazo de forma distinta porque un lado construyó el timestamp a partir de componentes de hora local. Cualquier camino de código que va de campos de fecha/hora locales → timestamp debe especificar la zona horaria de esos campos explícitamente. new Date(2026, 6, 28) en JavaScript significa medianoche en la zona horaria local del navegador, que es un instante distinto para cada usuario.

3. Bordes del horario de verano

DST crea dos tipos de horas de reloj rotas:

  • Horas inexistentes. En zonas horarias de EE. UU., 2026-03-08 02:30 local no existe — los relojes saltan 02:00 → 03:00. Un programador estilo cron configurado para «ejecutar a las 02:30 todos los días» saltará, se ejecutará dos veces, o se comportará de forma dependiente de la plataforma ese día.
  • Horas ambiguas. El 2026-11-01, 01:30 local ocurre dos veces en zonas horarias de EE. UU. cuando los relojes retroceden. Almacenar «01:30» sin offset o zona horaria no puede distinguir cuál quisiste decir.

Los arreglos robustos: programa trabajos recurrentes en UTC, almacena instantes como timestamps y solo convierte a hora local al mostrar. Si debes almacenar horas de reloj locales futuras (p. ej., «reunión a las 9 AM del usuario»), almacena la hora local más el nombre de zona horaria IANA (America/New_York) y resuelve a un timestamp en el momento de renderizar — porque los gobiernos cambian las reglas de DST, y un timestamp calculado hoy para una reunión de 2028 puede ser incorrecto tras un cambio legal.

4. Meses con índice cero de JavaScript

new Date(2026, 6, 28) es julio 28, no junio 28 — los meses van de 0–11 mientras los días van de 1–31. Esta inconsistencia ha producido una generación de bugs de off-by-one-month. Mitigaciones: usa Date.UTC(...) cuando quieras decir UTC, usa cadenas ISO (new Date("2026-07-28T00:00:00Z")) para mayor claridad, o usa una biblioteca (Temporal, cuando llegue ampliamente, lo arregla propiamente con meses indexados desde 1).

5. Precisión de float en segundos

El time.time() de Python devuelve un float. Un float64 tiene 53 bits de mantisa; a magnitudes de época actuales (~1.79 × 10^9 segundos), la resolución es buena (~240 nanosegundos), así que esto rara vez muerde en Python — pero el mismo razonamiento muerde en sistemas que llevan timestamps de microsegundos o nanosegundos como float64: un valor de microsegundos de 16 dígitos necesita 54 bits para representarse exactamente, así que los microsegundos en float64 ya son imprecisos hoy. Usa enteros para cualquier cosa más fina que milisegundos.

6. Parseo de cadenas sin offset

new Date("2026-07-28") en JavaScript parsea como medianoche UTC, pero new Date("2026-07-28 00:00:00") (sin T, sin Z) parsea como medianoche local — un cambio silencioso de zona horaria basado en detalles del formato de la cadena. Incluye siempre un Z explícito o un offset numérico en los datetimes serializados. Prefiere ISO 8601 con offset: 2026-07-28T10:00:00+08:00.

Obtener el timestamp correctamente, por lenguaje

JavaScript / TypeScriptDate.now() devuelve milisegundos; divide para segundos:

const ms = Date.now();                      // 1785235837123
const seconds = Math.floor(Date.now() / 1000); // 1785235837
// Desde un instante específico, especifica UTC siempre explícitamente:
const ts = Date.UTC(2026, 6, 28, 0, 0, 0) / 1000; // ¡meses con índice 0!

Pythontime.time() devuelve segundos en float; usa datetime con UTC explícito para cualquier cosa que vayas a almacenar:

import time
from datetime import datetime, timezone

seconds = int(time.time())                    # 1785235837
ns = time.time_ns()                           # nanosegundos, entero exacto
ts = int(datetime.now(timezone.utc).timestamp())

Go — el paquete time te da cada unidad explícitamente:

now := time.Now()
seconds := now.Unix()       // int64 segundos
millis  := now.UnixMilli()  // int64 milisegundos
nanos   := now.UnixNano()   // int64 nanosegundos — cuida el rango de int64

Java — evita Date/Calendar legados; usa java.time:

long millis  = System.currentTimeMillis();
long seconds = Instant.now().getEpochSecond();
long nanos   = Instant.now().getNano(); // nano-del-segundo, ¡no nanos de época!

Bash — GNU vs macOS (BSD) date difieren al parsear:

date +%s                        # segundos de época actuales (ambos)
date -d @1785235837             # GNU: timestamp → fecha humana
date -r 1785235837              # macOS/BSD: timestamp → fecha humana
date -u -d "2026-07-28 00:00:00 UTC" +%s   # GNU: fecha → timestamp
date -u -j -f "%Y-%m-%d %H:%M:%S" "2026-07-28 00:00:00" +%s  # macOS

Observa el tema recurrente: toda API correcta te da el valor de época directamente o te obliga a nombrar la zona horaria. Toda API peligrosa te permite omitirla.

Trabajando con timestamps interactivamente

Cuando depures un timestamp de un log o respuesta de API, no hagas los cálculos mentalmente. Pégalo en nuestro Conversor de Timestamps: muestra el instante en UTC y en tu zona horaria local, y autodetecta segundos vs milisegundos — que es exactamente la comprobación de ambigüedad descrita arriba. También convierte en la otra dirección (fecha humana + zona horaria → timestamp) para construir fixtures de prueba.

Resumen

  • Un timestamp Unix es segundos (o ms/µs/ns) desde 1970-01-01 UTC. No tiene zona horaria ni segundos intercalares.
  • El conteo de dígitos identifica la unidad en la era moderna: 10 = segundos, 13 = milisegundos, 16 = microsegundos, 19 = nanosegundos.
  • 2038 desborda el time_t de 32 bits con signo en 2038-01-19 03:14:07 UTC. Los sistemas de 64 bits y los lenguajes mainstream están a salvo; firmware embebido, binarios legados de 32 bits y columnas MySQL TIMESTAMP son el riesgo residual.
  • Los patrones de bug reales: confusión de unidades, errores local-vs-UTC, bordes de DST, meses con índice cero de JS, pérdida de precisión float64 en µs/ns y parseo de cadenas sin offset.
  • Almacena instantes como timestamps enteros, nombra campos con unidades, programa en UTC, convierte a local solo al mostrar — y ten un Conversor de Timestamps en marcadores para la depuración que se cuele de todas formas.