Sumar nueve horas es fácil; saber en qué lado está la fecha no lo es

La relación entre UTC y la hora de Japón es de las más simples que existen: JST = UTC + 9, siempre. Japón no aplica horario de verano, así que el desfase nunca cambia con la estación.

Lo que sí cambia es el lado español. España está en UTC+2 en verano (CEST) y en UTC+1 en invierno (CET), de modo que la diferencia con Japón oscila: 7 horas en verano y 8 horas en invierno. Ese salto de una hora, dos veces al año, es el origen de buena parte de los incidentes al integrarse con servicios japoneses.

Y aun con la aritmética resuelta, los fallos de zona horaria siguen ocurriendo, porque el problema real no es calcular: es perder la pista de a qué lado pertenece una marca de tiempo. Con los logs en UTC, la base de datos en hora local, la CI en UTC y el portátil en hora de Madrid, la cadena 2026-08-06 09:00:00 no dice nada por sí sola.

Tabla de desfases

UTCJapón (JST)España (verano, CEST)
00:0009:0002:00
03:0012:0005:00
07:0016:0009:00
12:0021:0014:00
15:00día siguiente 00:0017:00
22:00día siguiente 07:00día siguiente 00:00

En sentido inverso (JST → UTC) se restan nueve horas. De 00:00 a 08:59 en Japón corresponde al día anterior en UTC.

El cambio de día está en UTC 15:00 para Japón y en UTC 22:00 para España en verano (UTC 23:00 en invierno). Casi todos los errores de un día de desfase en procesos por lotes y en informes vienen de cruzar esa línea. Si quiere «un día completo de hora japonesa», en UTC ese rango es día anterior 15:00 → día actual 15:00.

Qué significan Z y +09:00 en ISO 8601

NotaciónSignificado
2026-08-06T00:00:00ZMedianoche UTC. Z indica UTC (hora Zulú)
2026-08-06T09:00:00+09:00Exactamente el mismo instante, escrito en JST
2026-08-06T09:00:00Sin desplazamiento: ambiguo. Lo interpreta cada analizador
2026-08-06Solo fecha. La hora y la zona dependen de la implementación

Las dos primeras describen el mismo instante. Mientras haya desplazamiento, ninguna notación pierde información.

La tercera fila es la peligrosa. Una fecha y hora sin desplazamiento no significa nada por sí misma. Si ve ese formato en una respuesta de API o en un CSV, consulte la especificación antes de asumir nada.

Cuándo desplaza JavaScript sus fechas

Es la trampa con la que más se tropieza: dos cadenas que parecen la misma fecha se convierten en instantes distintos.

// Una cadena de solo fecha se interpreta como UTC
new Date('2026-08-06').toISOString();
// => '2026-08-06T00:00:00.000Z'   (las 02:00 en España en verano)

// Una fecha y hora sin desplazamiento se interpreta como hora LOCAL
new Date('2026-08-06T00:00:00').toISOString();
// => '2026-08-05T22:00:00.000Z'   (ejecutado en España en verano: la fecha retrocede)

Añadir T00:00:00 cambia el resultado y hasta el día. Es el comportamiento especificado, no un error: solo fecha es UTC; fecha y hora sin desplazamiento es local.

La solución es ser siempre explícito:

new Date('2026-08-06T00:00:00+09:00').toISOString();
// => '2026-08-05T15:00:00.000Z'   (lo previsto: medianoche del 6 en Japón)

Para mostrar la hora en una zona concreta, pásela a toLocaleString. Así el resultado deja de depender de dónde se ejecute el código:

const d = new Date('2026-08-06T00:00:00Z');

d.toLocaleString('es-ES', { timeZone: 'Asia/Tokyo' });     // => '6/8/2026, 9:00:00'
d.toLocaleString('es-ES', { timeZone: 'Europe/Madrid' });  // => '6/8/2026, 2:00:00'

Recuerde además que toISOString() emite siempre UTC. Usarlo sobre lo que usted considera una hora local guarda un valor desplazado sin avisar.

Conversión en el servidor

Python

from datetime import datetime, timezone
from zoneinfo import ZoneInfo  # Python 3.9+

now_utc = datetime.now(timezone.utc)
now_jst = now_utc.astimezone(ZoneInfo('Asia/Tokyo'))     # España: 'Europe/Madrid'

# Regla: no deje entrar datetimes naive (sin tzinfo) en el sistema

MySQL

SELECT CONVERT_TZ('2026-08-06 00:00:00', '+00:00', '+09:00');
-- => 2026-08-06 09:00:00

Lo que hay que vigilar en MySQL es que las columnas TIMESTAMP se convierten según la zona horaria de la sesión, mientras que las DATETIME no se convierten nunca. Mezclar ambos tipos en una tabla deja columnas que parecen iguales pero significan cosas distintas. Las zonas con nombre ('Europe/Madrid') requieren cargar las tablas de zonas horarias, así que indicar el desplazamiento es más fiable. Con zonas que aplican horario de verano, el desplazamiento fijo solo es válido para la mitad del año: use nombres de zona cuando la fecha importe.

PostgreSQL

SELECT TIMESTAMPTZ '2026-08-06 00:00:00+00' AT TIME ZONE 'Asia/Tokyo';
-- => 2026-08-06 09:00:00

En PostgreSQL lo idiomático es almacenar TIMESTAMPTZ y convertir con AT TIME ZONE al mostrar. La elección de tipos entre bases de datos está recogida en la referencia de CREATE TABLE.

Cuatro sitios donde esto se rompe

1. El cron corre en la zona horaria del servidor

El schedule de GitHub Actions es siempre UTC. Escribir 0 9 * * * pensando en las nueve de la mañana en España lo ejecuta a las 11:00 en verano.

on:
  schedule:
    # 09:00 en España (verano, UTC+2) = 07:00 UTC
    - cron: '0 7 * * *'

Y aquí aparece un detalle que solo sufren las zonas con horario de verano: esa misma expresión se ejecutará a las 08:00 hora española en invierno, porque el cron no cambia con el cambio de hora. Si la hora local es un requisito del negocio, conviene programarlo con margen o comprobar la hora local dentro del propio trabajo. Para la sintaxis de cron, consulte cómo escribir expresiones cron; para las dependencias entre trabajos, patrones de needs en GitHub Actions.

2. Los contenedores Docker vienen en UTC

La mayoría de las imágenes base no traen configuración de zona horaria y funcionan en UTC. Por eso las marcas de tiempo de los logs difieren entre su equipo y el contenedor. Si la aplicación trata las horas como «locales», su comportamiento cambia según el entorno.

3. Logs con zonas horarias mezcladas

Logs de acceso en hora local del servidor, logs de aplicación en UTC y un panel de monitorización que renderiza en la zona del navegador: es una situación completamente normal. Al correlacionar logs durante una incidencia, normalice todo a UTC o a tiempo Unix antes de compararlos. Una marca de tiempo Unix no tiene zona horaria: es un número absoluto de segundos y por eso sirve de denominador común (véase qué es el tiempo Unix).

4. Guardar «solo la fecha»

Un valor como 2026-08-06 abarca un intervalo distinto en cada zona. Las fechas de negocio —cierres de facturación, días de informe— son más seguras como fechas en una columna DATE. En cuanto se convierten en fecha y hora, hay que responder a «medianoche, ¿de qué zona?».

La regla: guardar en UTC, convertir al mostrar

Resumido en tres líneas:

  1. Almacene y calcule en UTC (o en tiempo Unix)
  2. Convierta a la hora local solo al mostrar
  3. Incluya siempre el desplazamiento en las cadenas (Z o +09:00)

«Como el servicio es nacional, guardamos la hora local» suena razonable hasta que se recuerda que la CI y los contenedores funcionan en UTC. Cada frontera es una oportunidad de equivocarse; cuantas menos, mejor.

Convertir sobre la marcha

Cuando necesite saber qué hora japonesa corresponde a una marca UTC de un log, péguela en el conversor UTC/JST y obtendrá ambos sentidos a la vez. Admite formato ISO 8601 y también toma la hora actual.

Para marcas de tiempo Unix (números como 1785974400) use el conversor de tiempo Unix, y para leer valores de timeout o TTL entre segundos, minutos y horas, la conversión de unidades de tiempo. Todo se ejecuta dentro del navegador: las fechas que introduce no se envían a ningún sitio.

Resumen

  • JST = UTC + 9, fijo: Japón no aplica horario de verano
  • La diferencia España–Japón es de 7 horas en verano y 8 en invierno, porque quien cambia es España
  • El cambio de día japonés está en UTC 15:00: «un día japonés» va de las 15:00 del día anterior a las 15:00 del actual
  • Una cadena ISO 8601 sin desplazamiento (Z / +09:00) es ambigua
  • JavaScript interpreta solo-fecha como UTC y fecha-hora-sin-desplazamiento como local
  • MySQL convierte TIMESTAMP pero nunca DATETIME
  • El cron de GitHub Actions es siempre UTC y no sigue el cambio de hora
  • Guarde en UTC, convierta al mostrar y escriba siempre el desplazamiento

Preguntas frecuentes

¿Cuántas horas de diferencia hay entre España y Japón?

Siete horas en verano y ocho en invierno. Japón está fijo en UTC+9 y no aplica horario de verano, mientras que España alterna entre UTC+2 (CEST, verano) y UTC+1 (CET, invierno). La diferencia cambia porque cambia España, no Japón.

¿2026-08-06T09:00:00+09:00 y 2026-08-06T00:00:00Z son horas distintas?

Son el mismo instante escrito en dos zonas diferentes. Ninguna de las dos notaciones pierde información, pero conviene elegir una forma canónica dentro del sistema para evitar confusiones al comparar y ordenar.

¿Por qué se me desplaza la fecha un día en JavaScript?

Porque una cadena de solo fecha ('2026-08-06') se interpreta como UTC y una cadena con hora pero sin desplazamiento ('2026-08-06T00:00:00') se interpreta como hora local. En España en verano, la segunda apunta a las 22:00 UTC del día anterior, así que al imprimirla en UTC aparece la fecha anterior. Incluya siempre el desplazamiento.

¿Cómo ejecuto un trabajo de GitHub Actions a las 9:00 hora de España?

Con - cron: '0 7 * * *' durante el horario de verano (UTC+2). Tenga en cuenta que schedule se interpreta en UTC y no sigue el cambio de hora: en invierno esa misma expresión se ejecutará a las 08:00 hora local. Además, las ejecuciones programadas pueden retrasarse varios minutos por carga, así que evítelas cuando necesite precisión al minuto.

¿Se envían a un servidor las fechas que pego?

No. Tanto el conversor UTC/JST como el conversor de tiempo Unix funcionan íntegramente en su navegador: nada de lo que introduce se transmite a ningún sitio.