Dos ideas que suelen confundirse
Casi todos los errores con fechas vienen de mezclar dos conceptos distintos:
- Un instante es un punto único en la línea del tiempo, el mismo para todo el planeta. "El momento en que se registró este pago" es un instante. Un timestamp Unix como
1759672800representa un instante. - Una hora de pared (wall clock time) es lo que marca un reloj en un lugar concreto: "5 de octubre a las 09:00". Por sí sola no identifica un instante, porque las 09:00 en Bogotá y las 09:00 en Madrid son momentos separados por siete horas.
Para pasar de una hora de pared a un instante hace falta saber en qué zona está ese reloj. Para pasar de un instante a una hora de pared, hace falta saber para qué zona se quiere mostrar. Esta guía explica cómo hacer bien esas dos conversiones, qué pasa con el horario de verano y qué conviene guardar en una base de datos. Si lo que se necesita es convertir un valor concreto, el conversor de timestamp Unix hace las dos operaciones en el navegador.
UTC, desfase y zona horaria no son lo mismo
Son tres términos que se usan como sinónimos y no lo son:
| Término | Qué es | Ejemplo |
|---|---|---|
| UTC | La escala de tiempo de referencia mundial. No tiene horario de verano. | 2025-10-05T14:00:00Z |
| Desfase (offset) | Cuánto se adelanta o atrasa un reloj local respecto de UTC en un instante dado. | -05:00, +02:00 |
| Zona horaria | Las reglas de un lugar: qué desfase usa en cada fecha, incluidos los cambios de horario pasados y futuros. | America/Bogota, Europe/Madrid |
La diferencia entre desfase y zona es la más importante. Europe/Madrid usa +01:00 en invierno y +02:00 en verano; saber que una hora tiene desfase +02:00 no dice si es Madrid en verano, Johannesburgo o El Cairo. Las zonas se identifican con los nombres de la base de datos de zonas horarias de IANA (Continente/Ciudad), que es la que usan los sistemas operativos, los navegadores y casi todos los lenguajes.
Las abreviaturas como CST o IST conviene evitarlas por completo: CST puede ser la hora central de Estados Unidos (UTC−6), la hora de China (UTC+8) o la de Cuba (UTC−5); IST puede ser India, Irlanda o Israel.
Horario de verano: horas que no existen y horas que se repiten
Dos veces al año, en las zonas con horario de verano, la relación entre hora de pared e instante deja de ser uno a uno. En la Unión Europea el cambio ocurre a la 01:00 UTC del último domingo de marzo y del último domingo de octubre. En Madrid, en 2026:
| Fecha | Qué pasa a las 02:00 locales | Consecuencia |
|---|---|---|
| 29 de marzo de 2026 | El reloj salta de 02:00 a 03:00 | Las horas de 02:00 a 02:59 no existen ese día |
| 25 de octubre de 2026 | El reloj vuelve de 03:00 a 02:00 | Las horas de 02:00 a 02:59 ocurren dos veces |
El 25 de octubre, "02:30 en Madrid" corresponde a dos instantes distintos: 2026-10-25T02:30+02:00 (timestamp 1792888200) y, una hora después, 2026-10-25T02:30+01:00 (timestamp 1792891800). Sin el desfase, la hora local es ambigua. El 29 de marzo, en cambio, "02:30 en Madrid" no corresponde a ningún instante, y cada biblioteca resuelve el caso a su manera: algunas avanzan a las 03:30, otras lanzan un error.
Esto tiene consecuencias prácticas:
- Un día no siempre tiene 24 horas. El día del cambio dura 23 o 25 horas. Sumar
86400segundos a un timestamp no equivale a "mismo horario, día siguiente" si en medio hay un cambio de horario; para eso hay que sumar un día en el calendario de la zona. - Las tareas programadas en esa franja se comportan raro. Un trabajo de cron a las 02:30 puede no ejecutarse o ejecutarse dos veces según la implementación. La guía Crontab: 20 ejemplos reales explicados explica cómo evitarlo.
- Los informes por hora tienen un hueco o una hora doble si agrupan por hora local en lugar de por hora UTC.
Qué guardar en la base de datos
La regla general tiene dos partes, según si el evento ya ocurrió o está programado para el futuro.
Eventos que ya ocurrieron (un pago, un login, una línea de log): se guarda el instante, en UTC. Es inmutable: el pago ocurrió en ese momento y ninguna regla de zona horaria futura lo va a cambiar. En PostgreSQL, el tipo correcto es timestamptz (que guarda un instante y lo muestra convertido a la zona de la sesión), no timestamp a secas (que guarda una hora de pared sin zona). En MySQL, DATETIME con la convención de guardar siempre UTC es más seguro que TIMESTAMP, cuyo rango termina en 2038.
Eventos futuros con hora local (una reunión los lunes a las 10:00 en Santiago, el cierre de una tienda a las 21:00): se guarda la hora de pared y la zona IANA, y el instante se calcula cuando hace falta. La razón es que las reglas de las zonas cambian por decisión política, a veces con pocas semanas de aviso. México eliminó el horario de verano en casi todo el país en octubre de 2022; Chile modificó varias veces las fechas de su cambio de horario. Si una reunión de 2027 se guardó como instante UTC con las reglas viejas, después de un cambio de reglas va a aparecer una hora corrida.
Errores clásicos en el código
JavaScript: fecha sola es UTC, fecha con hora es local. Por especificación, new Date("2026-10-05") se interpreta como medianoche UTC, mientras que new Date("2026-10-05T00:00") se interpreta como medianoche local. En cualquier zona de América, la primera se muestra como el 4 de octubre. Además, en el constructor numérico los meses empiezan en cero: new Date(2026, 9, 5) es el 5 de octubre.
JavaScript: segundos y milisegundos. Date trabaja en milisegundos y casi todas las APIs devuelven segundos. new Date(1759672800) da una fecha de enero de 1970; lo correcto es new Date(1759672800 * 1000).
Python: fechas "ingenuas". Un datetime sin tzinfo no sabe en qué zona está, y Python lo trata como hora local del servidor en algunas operaciones y como UTC en otras. datetime.utcnow() devuelve justamente uno de esos objetos sin zona y está obsoleto desde Python 3.12. La forma correcta es datetime.now(timezone.utc) para el instante actual y zoneinfo.ZoneInfo("America/Bogota") para trabajar con zonas.
Comparar fechas como texto. "2026-10-05T09:00-05:00" y "2026-10-05T14:00Z" son el mismo instante, pero como cadenas no son iguales y ni siquiera se ordenan bien entre sí. Antes de comparar u ordenar, hay que normalizar a UTC o a timestamp.
Calcular la zona por el desfase del navegador. getTimezoneOffset() devuelve el desfase de hoy, no la zona. Para saber la zona del usuario, se usa Intl.DateTimeFormat().resolvedOptions().timeZone, que devuelve el nombre IANA.
Un ejemplo completo
Una aplicación tiene que mostrar a un usuario en Ciudad de México la hora de un evento registrado con timestamp 1759672800:
- El timestamp es un instante:
2025-10-05T14:00:00Z. - La zona del usuario es
America/Mexico_City. Desde 2022 no tiene horario de verano, así que su desfase es−06:00todo el año. - La hora de pared es
2025-10-05 08:00.
El mismo instante, mostrado a un usuario en Madrid (Europe/Madrid, que en octubre todavía está en horario de verano, +02:00), es 2025-10-05 16:00. Las tres representaciones pueden comprobarse cargando el timestamp 1759672800 en el conversor y cambiando la zona.
Lista de verificación
- Servidores, bases de datos y logs en UTC. La conversión a hora local se hace solo al mostrar, en el borde (la interfaz o el informe).
- Instantes pasados como UTC; eventos futuros como hora local + zona IANA.
- Zonas por nombre IANA, nunca por abreviatura ni por desfase fijo.
- ISO 8601 con desfase o
Zpara todo intercambio en texto (APIs, CSV, JSON). Una fecha sin zona es una fecha ambigua. - Bibliotecas que conozcan las zonas:
IntlyTemporalen JavaScript,zoneinfoen Python,java.timeen Java. Mantener actualizados los datos de zonas del sistema operativo (tzdata). - Pruebas que crucen un cambio de horario. Los errores de zona horaria no aparecen en los tests que corren un martes cualquiera.
Resumen
Un timestamp es un instante; una hora local es un instante visto desde una zona. Para convertir entre los dos hace falta la zona IANA, no solo el desfase, porque el desfase cambia con el horario de verano y con las decisiones de cada país. Guardar instantes en UTC, guardar las citas futuras con su zona y convertir a hora local solo al mostrar elimina la mayoría de los errores con fechas.