tutoriales·11 분 읽기

Unix Timestamps y Zonas Horarias: el bug que aparece solo en producción

Epoch time, UTC vs zona local, el problema del año 2038, errores comunes al comparar fechas en JS/SQL.

DevToolsHub Team
·
Unix Timestamps y Zonas Horarias: el bug que aparece solo en producción

El incidente del viernes a las 5 PM: cuando el tiempo se rompe

Era viernes por la tarde. Nuestro sistema de reservas funcionaba perfectamente en desarrollo. Los tests pasaban. El código parecía sólido. Pero a las 5 PM, cuando los usuarios empezaron a hacer reservas para el fin de semana, el sistema empezó a rechazarlas todas con el error "fecha inválida".

El problema no era lógico. Era de tiempo. Los desarrolladores estábamos en Madrid (UTC+1), el servidor estaba en Frankfurt (UTC+1), pero la base de datos estaba configurada en UTC. El frontend enviaba fechas en hora local, el backend las guardaba como si fueran UTC, y cuando intentábamos compararlas, los cálculos fallaban por 1 hora.

Este tipo de bug es frustrantemente común. Los timestamps Unix parecen simples (un número entero), pero esconden complejidades que solo aparecen cuando tu sistema cruza fronteras, horarios de verano, o zonas horarias diferentes.

Este tutorial es mi guía para evitar estos bugs. No solo te diré qué es un timestamp Unix, sino que te explicaré las trampas que he aprendido por las malas, para que no pierdas tu viernes por la tarde depurando problemas de tiempo.


¿Qué es realmente un Unix Timestamp?

Un Unix timestamp (o epoch time) es simplemente el número de segundos que han pasado desde el 1 de enero de 1970 a las 00:00:00 UTC. Es un número entero, simple, universal.

1718784000  =  19 de junio de 2024, 12:00:00 UTC

La belleza de este sistema es su simplicidad:

  • Es un número entero → fácil de almacenar, comparar, calcular
  • Es independiente de zona horaria → 1718784000 es el mismo instante en Madrid, Nueva York, y Tokio
  • Es universal → todos los sistemas operativos y lenguajes lo soportan

Nuestra herramienta de conversor de timestamps te permite convertir entre timestamps y fechas legibles en cualquier zona horaria.


La trampa #1: Confundir timestamp con fecha local

Esta es la trampa más común. Un timestamp representa un instante absoluto en UTC. Una fecha local representa ese instante visto desde una zona horaria específica.

El problema

// Timestamp: 1718784000 (19 de junio de 2024, 12:00:00 UTC)

// En Madrid (UTC+2 en verano)
new Date(1718784000 * 1000).toLocaleString('es-ES');
// "19/6/2024, 14:00:00"  → 2 PM, no 12 PM

// En Nueva York (UTC-4 en verano)
new Date(1718784000 * 1000).toLocaleString('en-US');
// "6/19/2024, 8:00:00 AM"  → 8 AM, no 12 PM

El mismo timestamp representa el mismo instante absoluto, pero se muestra diferente según la zona horaria.

La solución práctica

Regla de oro: Almacena timestamps en UTC, convierte a local solo para mostrar.

// ✅ CORRECTO: Almacenar en UTC
const createdAt = Math.floor(Date.now() / 1000);  // Timestamp en UTC
db.users.create({ createdAt });

// ✅ CORRECTO: Convertir a local solo para mostrar
function formatDateForUser(timestamp, userTimezone) {
  return new Date(timestamp * 1000).toLocaleString('es-ES', {
    timeZone: userTimezone
  });
}

// ❌ INCORRECTO: Almacenar fecha local como si fuera UTC
const localDate = new Date().toISOString();  // Esto incluye offset de zona
db.users.create({ createdAt: localDate });  // Desastre si el servidor cambia de zona

La trampa #2: JavaScript Date usa milisegundos, no segundos

Esta es la trampa que más me ha costado depurar. JavaScript Date usa milisegundos desde epoch. Los timestamps Unix usan segundos. La diferencia es un factor de 1000.

El problema

// Timestamp Unix: 1718784000 (segundos)
const timestamp = 1718784000;

// ❌ INCORRECTO: Pasar segundos directamente a Date
new Date(timestamp);
// Date: 20 de enero de 1970  → ¡Completamente erróneo!

// ✅ CORRECTO: Multiplicar por 1000 para convertir a milisegundos
new Date(timestamp * 1000);
// Date: 19 de junio de 2024  → Correcto

La solución práctica

Siempre multiplica por 1000 al crear un Date desde un timestamp Unix:

function timestampToDate(timestamp) {
  return new Date(timestamp * 1000);
}

function dateToTimestamp(date) {
  return Math.floor(date.getTime() / 1000);
}

La trampa #3: El problema del año 2038

Los timestamps Unix de 32 bits tienen un límite. El máximo valor representable es 2147483647, que corresponde al 19 de enero de 2038 a las 03:14:07 UTC. Después de eso, los timestamps de 32 bits "desbordan" y se convierten en números negativos, lo que representa fechas en 1901.

El problema

// Timestamp máximo de 32 bits
const max32bit = 2147483647;
new Date(max32bit * 1000);
// "19/1/2038, 3:14:07"

// Un segundo después
const overflow = 2147483648;
new Date(overflow * 1000);
// "13/12/1901, 20:45:52"  → ¡Viaje al pasado!

La solución práctica

Usa timestamps de 64 bits. JavaScript Date usa internamente números de 64 bits, así que no tienes este problema en el navegador o Node.js moderno. Pero si estás trabajando con:

  • Bases de datos antiguas con campos INT (32 bits)
  • Sistemas embebidos o lenguajes con integers de 32 bits
  • APIs externas que usan timestamps de 32 bits

Necesitas migrar a BIGINT o usar un esquema diferente.


La trampa #4: Horario de verano (DST)

El horario de verano es una pesadilla para los timestamps. No todas las zonas horarias lo usan. Las fechas de cambio varían por país y año. Y el mismo instante puede tener dos representaciones locales diferentes.

El problema

// En España, el horario de verano cambia el último domingo de marzo
// 2024: 31 de marzo a las 2:00 AM → 3:00 AM

const beforeDST = new Date('2024-03-31T01:59:59+01:00');
const afterDST = new Date('2024-03-31T03:00:00+02:00');

// Ambos representan el mismo instante absoluto
console.log(beforeDST.getTime() === afterDST.getTime());
// true  → ¡Mismo timestamp!

// Pero se muestran diferente
console.log(beforeDST.toLocaleString('es-ES'));
// "31/3/2024, 1:59:59"

console.log(afterDST.toLocaleString('es-ES'));
// "31/3/2024, 3:00:00"

La solución práctica

Nunca confíes en cálculos con hora local. Siempre trabaja en UTC:

// ❌ INCORRECTO: Calcular con hora local
const localDate = new Date('2024-03-31T02:00:00');  // ¡Esta hora no existe!
// JavaScript la ajusta automáticamente, pero el resultado es impredecible

// ✅ CORRECTO: Calcular en UTC
const utcDate = new Date('2024-03-31T01:00:00Z');  // Z explícito = UTC
const timestamp = Math.floor(utcDate.getTime() / 1000);

La trampa #5: Comparar fechas en diferentes zonas horarias

Cuando comparas fechas que vienen de diferentes zonas horarias, es fácil introducir bugs sutiles.

El problema

// Usuario en Madrid crea un evento para "hoy a las 6 PM"
const madridEvent = new Date('2024-06-19T18:00:00+02:00');

// Usuario en Nueva York ve el evento
const nyView = madridEvent.toLocaleString('en-US', { timeZone: 'America/New_York' });
// "6/19/2024, 12:00:00 PM"  → 12 PM, no 6 PM

// Si el usuario de NY intenta crear un evento "a las 6 PM su hora"
const nyEvent = new Date('2024-06-19T18:00:00-04:00');

// Comparar timestamps
console.log(madridEvent.getTime() === nyEvent.getTime());
// false  → Son instantes diferentes

La solución práctica

Almacena el instante absoluto (timestamp UTC), no la hora local:

// Al crear un evento, siempre guarda el timestamp UTC
function createEvent(localDate, userTimezone) {
  // Convertir fecha local a timestamp UTC
  const utcDate = new Date(localDate + 'Z');  // Forzar interpretación como UTC
  const timestamp = Math.floor(utcDate.getTime() / 1000);
  
  return db.events.create({
    timestamp,
    userTimezone  // Guardar la zona horaria del usuario para mostrar correctamente
  });
}

// Al mostrar, convertir de UTC a la zona del usuario
function showEvent(event, userTimezone) {
  return new Date(event.timestamp * 1000).toLocaleString('es-ES', {
    timeZone: userTimezone
  });
}

Implementación práctica: sistema de eventos robusto

Aquí está un patrón que uso para sistemas que manejan fechas en múltiples zonas horarias:

class EventSystem {
  constructor() {
    this.events = [];
  }

  // Crear evento: siempre recibir fecha en UTC
  createEvent(utcTimestamp, title) {
    const event = {
      id: Date.now().toString(),
      timestamp: utcTimestamp,  // Siempre UTC
      title
    };
    this.events.push(event);
    return event;
  }

  // Crear evento desde fecha local del usuario
  createEventFromLocal(localDateString, userTimezone, title) {
    // Interpretar la fecha local en la zona del usuario
    const localDate = new Date(localDateString);
    
    // Convertir a timestamp UTC
    const utcTimestamp = Math.floor(
      new Date(localDate.toLocaleString('en-US', { timeZone: 'UTC' })).getTime() / 1000
    );
    
    return this.createEvent(utcTimestamp, title);
  }

  // Mostrar evento en la zona del usuario
  formatEvent(event, userTimezone) {
    return {
      title: event.title,
      localTime: new Date(event.timestamp * 1000).toLocaleString('es-ES', {
        timeZone: userTimezone
      }),
      utcTime: new Date(event.timestamp * 1000).toISOString()
    };
  }

  // Buscar eventos en un rango (siempre en UTC)
  findEventsInRange(startUtcTimestamp, endUtcTimestamp) {
    return this.events.filter(event => 
      event.timestamp >= startUtcTimestamp && 
      event.timestamp <= endUtcTimestamp
    );
  }
}

// Uso
const system = new EventSystem();

// Usuario en Madrid crea evento para "hoy a las 6 PM su hora"
const event = system.createEventFromLocal(
  '2024-06-19T18:00:00',
  'Europe/Madrid',
  'Reunión de equipo'
);

// Usuario en Nueva York ve el evento
console.log(system.formatEvent(event, 'America/New_York'));
// {
//   title: "Reunión de equipo",
//   localTime: "6/19/2024, 12:00:00 PM",  → 12 PM en NY
//   utcTime: "2024-06-19T16:00:00.000Z"   → 4 PM UTC
// }

Timestamps en bases de datos

MySQL

-- Usar TIMESTAMP para almacenamiento automático en UTC
CREATE TABLE events (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(255),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  event_time TIMESTAMP
);

-- Consultar eventos en un rango UTC
SELECT * FROM events 
WHERE event_time BETWEEN '2024-06-19 00:00:00' AND '2024-06-19 23:59:59';

PostgreSQL

-- Usar TIMESTAMP WITH TIME ZONE para almacenamiento explícito
CREATE TABLE events (
  id SERIAL PRIMARY KEY,
  title TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  event_time TIMESTAMPTZ
);

-- Consultar eventos en un rango UTC
SELECT * FROM events 
WHERE event_time BETWEEN '2024-06-19 00:00:00+00' AND '2024-06-19 23:59:59+00';

MongoDB

// MongoDB usa Date objects internamente en UTC
db.events.insertOne({
  title: 'Reunión',
  createdAt: new Date(),  // Almacenado como UTC
  eventTime: new Date('2024-06-19T18:00:00Z')
});

// Consultar en rango UTC
db.events.find({
  eventTime: {
    $gte: new Date('2024-06-19T00:00:00Z'),
    $lte: new Date('2024-06-19T23:59:59Z')
  }
});

Herramientas útiles

Nuestra herramienta

Nuestra herramienta de conversor de timestamps te permite:

  • Convertir timestamps a fechas legibles en cualquier zona horaria
  • Convertir fechas a timestamps
  • Ver el mismo instante en múltiples zonas horarias simultáneamente

JavaScript

date-fns: Librería moderna para manipulación de fechas

npm install date-fns
import { format, utcToZonedTime } from 'date-fns';
import { es } from 'date-fns/locale';

const timestamp = 1718784000;
const date = new Date(timestamp * 1000);

// Formatear en zona específica
const zonedDate = utcToZonedTime(date, 'Europe/Madrid');
console.log(format(zonedDate, 'PPP p', { locale: es }));
// "19 de junio de 2024, 14:00:00"

Luxon: Librería de Moment.js mejorada

npm install luxon
import { DateTime } from 'luxon';

const timestamp = 1718784000;
const dt = DateTime.fromSeconds(timestamp).setZone('Europe/Madrid');
console.log(dt.toLocaleString(DateTime.DATETIME_FULL));
// "19 de junio de 2024, 14:00:00"

Checklist para evitar bugs de tiempo

Antes de implementar cualquier sistema que maneje fechas:

☐ Decide tu estrategia de almacenamiento

  • ¿Timestamps UTC o Date objects?
  • ¿Con o sin zona horaria?
  • Documenta esta decisión

☐ Define tu estrategia de visualización

  • ¿En qué zona horaria muestras las fechas?
  • ¿Es la zona del servidor, del usuario, o configurable?
  • ¿Cómo manejas usuarios en múltiples zonas?

☐ Prueba con múltiples zonas horarias

  • No solo pruebes en tu zona local
  • Prueba con UTC, EST, PST, JST
  • Prueba con zonas que no usan horario de verano

☐ Prueba el horario de verano

  • Prueba fechas alrededor de los cambios de DST
  • Verifica que los cálculos son consistentes
  • Asegúrate de que no hay "huecos" o "duplicados" en el tiempo

☐ Documenta las suposiciones

  • ¿Qué pasa si un usuario cambia de zona horaria?
  • ¿Qué pasa con eventos históricos antes de cambios de política de DST?
  • ¿Cómo manejas zonas horarias que no existen en el IANA database?

Conclusión: el tiempo es más complejo de lo que parece

Los timestamps Unix parecen simples: un número entero que representa un instante. Pero la realidad es que el tiempo es una de las cosas más complejas en informática. Zonas horarias, horario de verano, políticas gubernamentales, y sistemas heredados crean un ecosistema donde es fácil introducir bugs sutiles que solo aparecen en producción.

La regla que uso en todos mis proyectos: almacena en UTC, convierte a local solo para mostrar. Esta simple regla evita el 90% de los problemas de tiempo.

Nuestra herramienta de conversor de timestamps está diseñada para ayudarte a entender estas conversiones. Te permite ver el mismo instante en múltiples zonas horarias, lo que es invaluable para depurar problemas de tiempo.

La próxima vez que alguien te diga "es solo una fecha, qué puede salir mal", ya sabes la respuesta: mucho. Pero con las estrategias correctas, es completamente manejable.

#timestamps#fechas#timezones#javascript#debugging

관련 기사