Ir al contenido
AppsSDAR

Servidor en UTC, usuarios en Lima: el "hoy" que empieza a las 7 de la noche

5 min de lectura Hosting y despliegue

Un panel mostraba cuántos mensajes habían llegado "hoy" y un gráfico con los últimos días. Los números parecían casi correctos, que es la peor forma de estar mal. Un mensaje recibido a las 8:30 de la noche en Lima aparecía contado en el día siguiente. La causa era la de siempre: el servidor vive en UTC y los usuarios en Perú, cinco horas atrás.

El error

En PostgreSQL, la forma natural de filtrar lo de hoy es comparar la fecha del registro con la fecha actual:

WHERE created_at::date = CURRENT_DATE

Esa conversión a fecha se hace en la zona horaria de la sesión, que en el servidor es UTC. Para UTC, el día cambia a medianoche de Greenwich, que en Lima son las 7 de la noche. Resultado: para el panel, el "hoy" de un usuario peruano empieza a las 19:00 del día anterior.

La corrección

La fecha se calcula convirtiendo explícitamente a la zona horaria del negocio, tanto para el registro como para el momento actual:

WHERE (created_at AT TIME ZONE 'America/Lima')::date
    = (now() AT TIME ZONE 'America/Lima')::date

Lo mismo aplica a cualquier agrupación por día: gráficos de los últimos siete días, reportes diarios y crons que "cierran el día". Todo lo que diga "día" debe decir también de dónde.

Lo que no hay que tocar

Cuando se descubre este problema aparece la tentación de "arreglar" los datos guardados restándoles cinco horas. Sería un error grave. Una columna timestamptz guarda un instante absoluto, y ese instante siempre fue correcto. Lo que estaba mal era la forma de agruparlo al leerlo. Reescribir los timestamps corrompería el dato y rompería cualquier otra consulta que ya lo leía bien.

Guardar en UTC está bien. Agrupar en UTC para usuarios que no viven en UTC está mal.

La misma trampa en el navegador

JavaScript tiene su propia versión. Una fecha sin hora escrita en formato ISO se interpreta como medianoche UTC:

new Date('2026-08-12')              // medianoche UTC = 11 de agosto en Lima
new Date('2026-08-12' + 'T00:00:00') // medianoche local

Las etiquetas del gráfico estaban corridas un día por esta razón, sin que nadie lo notara, porque el desfase era consistente. Para fechas sin hora que vienen del servidor, agregar la hora explícita fuerza la interpretación local.

Un detalle de experiencia de usuario

Aun con los cálculos correctos, un gráfico por días puede parecer equivocado. Si alguien trabaja de madrugada, lo que vive como "anoche" cae en el día de hoy según el calendario. Etiquetar las dos últimas barras como hoy y ayer, en vez de con el nombre del día de la semana, reduce esa confusión sin cambiar los datos.

Cómo prevenirlo

  • Toda consulta que agrupe o filtre por día declara la zona horaria del negocio.
  • Toda fecha sin hora que llega al navegador se construye como local.
  • Al probar, crear registros entre las 19:00 y la medianoche de Lima: es la franja donde el error se hace visible.