Timestamps and time zones

PostgreSQL · Essentials · 12 min

Time zone bugs are among the most common data bugs; one rule about timestamptz prevents most of them.

PostgreSQL has two timestamp types, and the names mislead:

  • timestamp with time zone (timestamptz) stores an absolute instant, internally in UTC. On input, a value without an offset is interpreted in the session's TimeZone; on output it is converted to the session's TimeZone. It does not store the original zone.
  • timestamp (without time zone) stores a wall-clock reading with no zone at all. 2026-03-29 02:30 means nothing until you say where.

Rule: use timestamptz for events that happened at a moment (created_at, paid_at). Use timestamp or date + time only for civil times that must not shift, such as "the shop opens at 09:00 local time", and store the zone name in another column.

Useful tools:

  • now() returns the transaction's start time; clock_timestamp() the actual current time.
  • AT TIME ZONE 'Asia/Yerevan' converts a timestamptz into local wall-clock time in that zone (and the reverse for a plain timestamp).
  • interval arithmetic: now() - interval '7 days', age(a, b).
  • date_trunc('day', ts) for bucketing — it truncates in the session time zone.

Set the server or database TimeZone to UTC and convert at the edges, in the application or with AT TIME ZONE.

Example

SET TimeZone = 'UTC';
CREATE TABLE login (at timestamptz NOT NULL);
INSERT INTO login VALUES ('2026-03-10 18:30:00+04');   -- an instant
SELECT at FROM login;                       -- 2026-03-10 14:30:00+00

SET TimeZone = 'America/New_York';
SELECT at FROM login;                       -- 2026-03-10 10:30:00-04 (same instant)

SELECT at AT TIME ZONE 'Asia/Yerevan' FROM login;   -- 2026-03-10 18:30:00 (wall clock)
SELECT '2026-01-31'::date + interval '1 month';     -- 2026-02-28 00:00:00
SELECT date_trunc('hour', at) FROM login;           -- 2026-03-10 10:00:00-04

Remember

  • timestamptz stores an instant (UTC) and displays it in the session TimeZone
  • Use timestamptz for events; plain timestamp only for zone-less civil times
  • AT TIME ZONE converts between instants and local wall-clock times

Watch out for

  • Using timestamp without time zone for created_at and getting shifted values when servers or sessions change zone.

Check yourself

  1. A timestamptz column holds 12:00 UTC. A session with TimeZone = 'Europe/Paris' (UTC+1 in winter) selects it in January. What is shown?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.postgresql.org/docs/current/datatype-datetime.html