Common Time Zone Bugs in Databases and Server Logs

A surprising share of "the numbers don't match" bugs in backend systems trace back to how a database or a log line stores a moment in time — not to any actual data-processing error. Two habits prevent almost all of them: know which timestamp type your database column actually is, and never write a timestamp to a log without an explicit offset.

TIMESTAMP vs. TIMESTAMPTZ — They're Not Interchangeable

In PostgreSQL, a plain TIMESTAMP column stores a date and time with no time zone information — it's the database equivalent of a "naive" datetime in application code. A TIMESTAMPTZ column, despite what the name suggests, doesn't actually store a time zone either — it stores everything internally as UTC and converts to and from the session's configured time zone on the way in and out. The practical difference: insert a value into TIMESTAMPTZ from a session in one time zone and read it back from a session in a different time zone, and you'll see the same instant displayed as a different local time — correctly. Do the same with a plain TIMESTAMP column and the raw value never adjusts, because there was never any zone attached to convert from in the first place. MySQL has a similar split between DATETIME (no zone, no conversion) and TIMESTAMP (UTC-based, timezone-adjusted on read/write, but with a narrower valid date range — it can't represent dates after January 2038, the same Year 2038 problem covered on our Unix Timestamp Converter page).

The Practical Rule: Use the Timezone-Aware Type, Always

Unless there's a specific reason to store a genuinely zone-less value (a recurring local wall-clock event like "every day at 9am wherever the user is," where the zone should shift with the user rather than staying fixed), default to the timezone-aware column type and let the database handle the UTC conversion. Storing a naive value and trying to remember which zone it was written in in application code is exactly the ambiguity that causes bugs months later when a different engineer touches that table.

Log Timestamps: The Same Problem, Spread Across Servers

A log line without an explicit UTC offset is exactly as ambiguous as a database TIMESTAMP column, but the stakes are often higher, because logs from multiple servers frequently get merged and compared. If one server logs in its local time zone and another logs in UTC, correlating an event across both — "which service touched this request first?" — becomes guesswork the moment those servers aren't in the same zone, or one of them observes daylight saving and the other doesn't. Configuring every server and every logging library to emit RFC 3339 timestamps in UTC (see our ISO 8601 vs. RFC 3339 vs. Unix Timestamp guide) removes the ambiguity entirely and makes merged, multi-server log timelines trustworthy.

"Today" Means Different Things on Different Servers

A batch job that groups records by "today's date" is implicitly choosing a time zone boundary for midnight, whether or not that choice was deliberate. A server using its local system time zone for that grouping will disagree with a server in a different region about which records belong to "today" for several hours around each midnight — a classic source of off-by-one-day bugs in daily reports and reconciliation jobs that span multiple regions. Defining "day" against a single, explicit reference zone (usually UTC, or the business's own official reporting zone) for every server in the pipeline avoids the disagreement — see our Cron Jobs and Time Zones guide for the same principle applied to scheduling.

A Quick Diagnostic Checklist

  • Does every TIMESTAMP-family column in your schema either carry an explicit zone, or have a clear, documented convention for which zone naive values are written in?
  • Do all your servers and services log in the same zone (ideally UTC), regardless of where they're physically or geographically deployed?
  • Does any batch or reporting job group records by calendar date using a time zone that isn't explicitly chosen and documented?
  • Do date-only fields ("expires on 2026-08-01") ever get compared against full datetime values without first deciding what time that date actually means?

Related

To convert a raw timestamp value by hand while debugging, use our Unix Timestamp Converter. For the JavaScript-side equivalent of this problem, see Handling Timezones & UTC in JavaScript.

Tools

Articles & Guides