Tecnología
AT TIME ZONE 'UTC' en Postgres es una trampa: el bug de fechas que nadie detecta
Un motor de PostgreSQL con un backend serio, facturacion en dolares, en produccion desde hace tres anos. Cero errores. Cero alertas. Cero exceptions en los logs.
Y los reportes de ventas de cada mes le salen corrida una hora completa a todos los clientes de la costa este.
No hubo ningun deploy. No hubo ninguna dependencia rota. El bug estaba en el codigo desde el dia uno, escribiendo la fecha correcta en el tipo de columna equivocado. Tu base de datos no te avisa: PostgreSQL es internamente consistente, simplemente responde a una pregunta distinta a la que creias estar haciendo.
Esta semana ese fallo explicito se volvio el post mas discutido de Hacker News: 147 puntos y 85 comentarios en "Footguns with Postgres AT TIME ZONE 'UTC'". Y los comentarios son la parte interesante, porque no son quejas: son engineers senior relatando exactamente el mismo debug de tres anos.
El problema: la misma frase significa dos cosas opuestas
PostgreSQL tiene dos tipos de fecha que se parecen en el nombre y no se parecen en nada:
TIMESTAMP — sin zona horaria. Guarda solo la fecha y la hora: "2026-09-28 14:30:00". Es un par de numeros, no un momento del tiempo. No significa nada hasta que alguien decide en que zona interpretarlo.
TIMESTAMPTZ — con zona horaria. Guarda un instante real en el reloj. El nombre es una trampa en si mismo: no guarda la zona horaria. Guarda UTC y convierte segun la zona de la sesion cuando lo lees.
Y aqui esta la trampa real: el operador AT TIME ZONE significa cosas opuestas segun el tipo de la izquierda.
-- TIMESTAMPTZ AT TIME ZONE 'X' -> devuelve TIMESTAMP en la zona X
-- TIMESTAMP AT TIME ZONE 'X' -> devuelve TIMESTAMPTZ tomando la entrada como zona X
Leelo en voz alta. La misma sintaxis, uno convierte, el otro interpreta. No hay forma de que el SQL te diga cual de los dos esta pasando, porque Postgres no tiene opinion sobre cual era tu intencion.
El comentario mas votado del hilo lo propone exactamente: si la sintaxis fuera explicita en ambos sentidos, el problema no existiria. Algo como AS ZONED AT TIME ZONE para convertir, y AS LOCAL AT TIME ZONE para interpretar. El estandar SQL fallo en ser claro.
Por que 'UTC' es la peor zona posible para aprender
El articulo original se concentra en el caso AT TIME ZONE 'UTC', y el motivo es que es el error mas caro de toda la familia.
Un developer junior que escribe AT TIME ZONE 'America/Santiago' y se equivoca nota el problema en una semana: los horarios se ven raros, los usuarios se quejan. Es un error ruidoso.
Pero AT TIME ZONE 'UTC' no produce nada raro. Produce una conversion que parece razonable y es un no-op disfrazado. En la mayoria de los casos, el resultado es numericamente identico a lo que tenias. Solo que ahora el tipo de dato cambio, y las comparaciones subsequentes empiezan a fallar en silencio.
Un comentario del hilo lo resume perfecto: la confusion viene de que "AT TIME ZONE" es sintaxis compartida por las dos operaciones de conversion. Si la usas con UTC, el error es invisible por diseno.
El segundo footgun: los meses no duran lo mismo
Este va mas alla de zonas horarias, y es el que mas caro sale en produccion.
La diferencia entre NOW() + INTERVAL '1 month' y NOW() + INTERVAL '30 days' no es academica. Si tu facturacion usa meses calendario y tu job de suscripciones usa intervalos fijos, cada mes tus ciclos se desalinean en un dia o dos. Al cabo de un ano, los clientes pagan el dia equivocado y tu soporte se llena de tickets.
Y hay una trampa adicional: agregar meses es timezone-dependent. Un comentario del hilo lo senala de forma explicita: sumar un mes no esta bien definido ni siquiera con DATE, porque no todos los meses tienen 30 dias. Y el resultado puede cambiar segun la zona horaria de la sesion.
Otro developer responde con la regla mas sana que leiste hoy: "No hago calculos de tiempo a nivel de base de datos. Los manejo en la aplicacion, sobre UTC almacenado, con el timezone del usuario." Menos puntos de contacto, menos sorpresas.
La regla de oro que todo el hilo acepta
El consenso de los 85 comentarios es simple:
Guarda SIEMPRE TIMESTAMPTZ. Convierte a zona local en la capa de aplicacion, jamas en el query.
El porque tecnico vale la pena por dentro: TIMESTAMPTZ almacena UTC puro. Una sesion con zona distinta no cambia como se guarda. Eso elimina de raiz una clase entera de bugs de fechas, porque el valor persistido es el mismo independientemente de quien escriba.
Y hay una razon menos obvia por la que nunca debes usar SET TIME ZONE 'PST' a nivel de sesion: Postgres solo puede almacenar tiempo sin zona, o UTC. Todo lo demas es una capa de conversion. Si horneas offsets fijos en tu infraestructura, el dia que un pais abole el horario de verano — o cambia sus reglas — tu app tiene corrupcion silenciosa de datos. Y no vas a enterarte hasta que un cliente te mande un invoice con fecha de mayo en marzo.
El angulo LATAM: por que esto nos pega mas fuerte
Nuestra region es la zona horaria mas tamiza del planeta para esto.
No hablo de los UTC-3, -4, -5 basicos. Hablo de la historia: Chile cambio de zona horaria, Mexico elimino el horario de verano hace anos, Argentina alterna, Bolivia cambio dos veces, y las reglas de America/Santiago se actualizan en la base de datos tzdb unas 10 veces al ano.
Cuando tu backend corre en un servidor en Virginia y tus usuarios estan en Santo Domingo, Lima o Montevideo, esa distancia es exactamente donde los bugs de timestamps se materializan. Un developer en el mismo zip code que el servidor jamas ve el problema. El QA en produccion con zona America/Santiago lo ve el primer dia.
Mi recomendacion concreta para cualquier equipo latino: configura tu servidor de base de datos y tu suite de tests en UTC desde el dia uno. No tu zona de trabajo. UTC. Y pon tests que corran al menos uno con SET TIME ZONE distinto para cada region donde operas.
Porque el costo de esto no es el bug. El bug se arregla en 20 minutos. El costo son los tres meses de facturacion corrida que nadie pudo explicar porque los logs estaban todos en verde.
Tu checklist de esta semana
1. grep -r "AT TIME ZONE" src/. Cada hit es un sitio donde verificar el tipo del lado izquierdo.
2. Toda columna de fecha en tu esquema deberia ser TIMESTAMPTZ. Si ves TIMESTAMP pelado, tienes una decision pendiente que alguien nunca tomo.
3. Saca los INTERVAL '30 days' de la logica de suscripciones y usa calendario explicito. "Un mes" no son 30 dias.
4. Nunca hornees offsets (-03:00, EST) en tu codigo. Guarda el offset de la sesion del usuario y calculalo al renderizar.
5. Un test que corra la misma query con tres SET TIME ZONE distintos y verifique que el instante subyacente es identico. Si no es identico, tienes el bug.
Mi opinion, sin filtro
Postgres no es la fuente del problema. Es el lugar donde un malentendido se vuelve irreversible.
El store esta bien disenado para lo que hace. El problema es que su diseno asume que la gente sabe lo que esta escribiendo, y el 90% de las personas que escriben SQL no lo sabe. El tipo TIMESTAMP sin zona deberia haberse llamado DATETIME desde el principio, como dice un comentista del hilo. Habria eliminado anos de confusion.
Mi takeaway para los que somos devs: la base de datos no va a lanzar excepcion por un error de zona horaria. Va a devolverte un numero plausible, un dashboard en verde, y un cliente quejandose tres meses despues. El unico antidoto es entender que la fecha que guardaste no es la fecha que crees que guardaste.
Comparte esto con ese dev de tu equipo que siempre pone AT TIME ZONE 'UTC' "por si acaso", porque ya casi le costo un cliente. Y dime en los comentarios: cual ha sido tu peor bug de fechas, y cuanto tiempo tardaron en encontrarlo?