Postgres 'AT TIME ZONE UTC' Changes Your Column Type in Both Directions
Software / news
Postgres 'AT TIME ZONE UTC' Changes Your Column Type in Both Directions
The PostgreSQL 18 documentation says the operator returns a different type than it receives, and a Hacker News thread of 140 points shows how that surprises people.

A developer who writes created_at AT TIME ZONE 'UTC' to get a UTC value gets a value with no time zone attached at all. The PostgreSQL 18 documentation says so directly, and a post titled "Postgres AT TIME ZONE 'UTC' does NOT do what you think it does" reached 140 points on Hacker News.
The Terminal could retrieve only that post's title, not its body, so everything below rests on the documentation page (version 18.6) and the discussion thread. Nothing here was run against a live server.
What the PostgreSQL 18.6 documentation says the operator returns
AT TIME ZONE is one operator with two meanings, chosen by the type on its left. The documentation lists both.
| Left-hand type | Result type | Docs example |
|---|---|---|
| timestamp with time zone | timestamp without time zone | 2001-02-16 20:38:40-05 at America/Denver gives 2001-02-16 18:38:40 |
| timestamp without time zone | timestamp with time zone | 2001-02-16 20:38:40 at America/Denver gives 2001-02-17 03:38:40+00 |
The second row is worth reading twice. The output ends in +00 because the example session displays in UTC, so the plain timestamp was read as Denver wall-clock time and then shown as an instant.
Why the UTC idiom quietly changes the type
A timestamptz column stores one instant. Feed it to the first row of the table and the result is a plain timestamp, which is a calendar reading with no zone. Commenter ulrikrasmussen put the objection in terms of the type names: a plain timestamp does not identify a unique point in time, while timestamptz does not actually store a zone, only the instant.
Commenter ScanMyTerms described the operator as a cast that means different things depending on its input, with the session TimeZone acting as a hidden third argument. That reading matches the two rows above.
Because the rows compose, applying the operator twice returns a timestamptz. That follows from the documented types rather than from a test the Terminal ran. It also explains why interval arithmetic done between the two applications happens on calendar values, not instants.
The comparison that depends on TimeZone
The original post argued that comparing timestamp with timestamptz is always false. Commenter heurekamala corrected that in the thread: Postgres converts one side using the session TimeZone, so the outcome depends on the setting. A query that matches on a developer's laptop can miss rows in production if the two sessions use different zones.
Commenter tibbar raised a harder case: a comparison mismatch around daylight saving transitions that can make queries using a btree index return wrong results. tibbar said the PostgreSQL mailing list confirmed it as a bug with no good fix. That is one commenter's account and the Terminal did not find the list thread.
What to write instead
Commenter paulddraper gave the rule most of the thread converged on: use timestamptz for instants, and use plain timestamp only for datetimes that are genuinely local, such as a store's opening hour.

If you only need a UTC calendar date for grouping, cast at the edge of the query and keep the stored column as timestamptz. The documentation page for 18.6 is the place to check the exact behaviour before a migration: it is the only source in this piece that anyone can verify in one click, and the tibbar claim about btree indexes is the one to test before trusting.
More on databases and data tooling
For other open-source data tooling that surfaced on GitHub trending this week, see our coverage of Hindsight's agent memory 0.10 release. For another open-source project that changed its own identity, see postmarketOS's rebrand as Nura.
Sources
More in Software
- 01Polyend Publishes Tracker Firmware Source Under a Noncommercial Licence, With Two Binary Libraries Left OutThe tracker-ce repository covers the original Tracker, Tracker+ and Tracker Mini, but the synth and DSP libraries ship precompiled under a separate proprietary licence.
- 02Claude-Mem's Installer Uploaded 30 Days of Prompts to cmem.ai Before Asking; v15.8.3 Adds a Consent PanelThe fix shipped on Nov. 2, but the release notes say prompts already uploaded by earlier versions are not removed, and the GitHub issue was reopened the same day.
- 03Vercel Labs' Agent-Desktop 1.0 Ships With a macOS Bug That Types Into the Frontmost AppThe Rust tool lets an AI agent drive desktop apps through the accessibility tree, and 48 open issues sit against it three days after the 1.0.2 release.
- 04T3 Code Reaches 38.1k Stars in Alpha, and Its Remote Access Docs Say to Treat Pairing Links as PasswordsThe MIT-licensed control surface for Claude Code, Codex and other agents has nearly doubled its star count since August 27. Its own documentation lists what stays risky.