Unix Timestamp Timezone Pitfalls and How to Avoid Them
Unix timestamps are supposed to be simple: one integer, representing seconds since January 1, 1970 UTC. No timezone. No ambiguity. Just a number.
And yet, timezone-related bugs in timestamp handling are everywhere. They show up as events logged at the wrong time, calendar appointments shifted by hours, or database queries that return the wrong rows. Most of them trace back to the same small set of mistakes.
This article walks through the common failure modes and how to avoid them. The Unix Timestamp Converter lets you check any timestamp against UTC and local time side by side.
Why Timestamps Seem Timezone-Safe — Until They Aren't
A Unix timestamp itself has no timezone. 1714000000 is the same moment everywhere. The trouble starts when you convert it to or from a human-readable date, because that conversion always involves a timezone — often an implicit one you didn't choose.
Most languages default to the system's local timezone when converting. If your server is set to UTC, everything looks fine. If it's set to New York time, every timestamp-to-date conversion is off by 5 hours. If the developer's laptop is in Berlin, results differ from the CI server in the US. The timestamp is consistent; the interpretation isn't.
Pitfall 1: Constructing Timestamps From Local Date Strings
This is the most common bug. You have a date string like "2024-04-25 09:00:00" and you convert it to a timestamp:
JavaScript: `js new Date("2024-04-25 09:00:00").getTime() / 1000 `
Python: `python from datetime import datetime datetime(2024, 4, 25, 9, 0, 0).timestamp() `
Both of these treat the input as a local time. If you're in UTC+2, the resulting timestamp represents 07:00 UTC, not 09:00 UTC. Your timestamp is off by two hours.
The fix is always to specify UTC explicitly:
// JavaScript — append Z or use Date.UTC()
new Date("2024-04-25T09:00:00Z").getTime() / 1000
// or
Date.UTC(2024, 3, 25, 9, 0, 0) / 1000
# Python — use timezone.utc
from datetime import datetime, timezone
datetime(2024, 4, 25, 9, 0, 0, tzinfo=timezone.utc).timestamp()
The difference looks small locally but breaks badly when your code runs in a different environment.
Pitfall 2: Displaying Timestamps Without Specifying a Timezone
The reverse of pitfall 1. You convert a stored timestamp back to a readable date for display:
new Date(1714000000 * 1000).toLocaleString()
// "4/25/2024, 7:46:40 AM" (on a UTC-4 machine)
// "4/25/2024, 1:46:40 PM" (on a UTC+2 machine)
The same timestamp shows a different time on different machines. This is often correct behavior for user-facing dates — showing local time to each user makes sense. But it's wrong for logs, audit records, API responses, and anything that needs to be consistent across systems.
For consistency, always render to UTC or specify the timezone explicitly:
new Date(1714000000 * 1000).toISOString()
// "2024-04-25T11:46:40.000Z" — always UTC, always consistent
from datetime import datetime, timezone
datetime.fromtimestamp(1714000000, tz=timezone.utc).isoformat()
# "2024-04-25T11:46:40+00:00"
Pitfall 3: Daylight Saving Time at the Boundaries
Daylight saving time (DST) transitions cause two specific problems.
The "repeated hour." When clocks fall back, a one-hour window occurs twice. If your system is logging in local time without timezone information and you try to convert those log entries to timestamps, some entries are ambiguous. 2:30 AM on the night of a fall-back could be either the first or second occurrence — two different timestamps.
The "skipped hour." When clocks spring forward, one hour simply does not exist in local time. A scheduled job at 2:30 AM on a spring-forward night will either fire early, fire late, or not fire at all, depending on how the scheduler handles the gap.
Neither of these problems affects Unix timestamps — the timestamp count continues uninterrupted through DST. The problem only arises when you convert between timestamps and local time strings. The fix is to store and process timestamps in UTC throughout your system, only converting to local time at the presentation layer.
Pitfall 4: Database Columns Storing the Wrong Format
PostgreSQL has two timestamp types: TIMESTAMP WITHOUT TIME ZONE and TIMESTAMP WITH TIME ZONE (also spelled TIMESTAMPTZ).
TIMESTAMP WITHOUT TIME ZONE stores whatever string you give it and makes no timezone adjustments. If you insert "2024-04-25 09:00:00" from a local client, that's exactly what gets stored — even if it was 09:00 in Chicago and you expected 14:00 UTC.
TIMESTAMPTZ converts to UTC on write and converts back to the session timezone on read. This is usually what you want, but it means the displayed value depends on SET TIME ZONE in the session.
For applications that need to avoid this complexity, storing timestamps as plain integers (Unix seconds) sidesteps the database timezone layer entirely. You control interpretation in the application layer.
MySQL's DATETIME vs TIMESTAMP types have a similar distinction. TIMESTAMP converts to UTC on storage and back on retrieval. DATETIME stores literally and does nothing with timezone.
Pitfall 5: Milliseconds vs Seconds Mismatch in APIs
This is less about timezones and more about scale, but it produces the same category of confusing timestamp bugs.
JavaScript's Date.now() returns milliseconds. Most server-side languages and Unix tools use seconds. A timestamp in seconds is currently a 10-digit number (~1,714,000,000). In milliseconds, it's 13 digits (~1,714,000,000,000).
When a JavaScript frontend sends a timestamp to a backend that expects seconds, the result is a date in the year 57,000. When a backend sends seconds to JavaScript and the frontend interprets it as milliseconds, dates show up in January 1970.
The Unix Timestamp Converter detects the format automatically based on digit count, which is useful for sanity-checking values from different systems.
The safest API convention is to document explicitly whether timestamps are seconds or milliseconds, and to validate incoming values server-side. A simple range check (reject anything below 1970 or above 2100 in UTC) catches most mis-scaled values before they corrupt records.
Pitfall 6: Parsing Ambiguous Date Strings
Not all date strings specify a timezone. "April 25, 2024", "04/25/2024", "2024-04-25" — none of these have timezone information. Different parsers handle them differently.
JavaScript is particularly inconsistent here. new Date("2024-04-25") treats the input as UTC midnight. new Date("04/25/2024") treats it as local midnight. The ISO 8601 format with a date-only string is the one exception to the local-default behavior.
Python's datetime.strptime("2024-04-25", "%Y-%m-%d") produces a naive datetime (no timezone). Calling .timestamp() on it uses local time. If you're running in UTC, it produces the right answer. If you're in a different zone, it doesn't.
The practical rule: never convert a date string to a timestamp without explicitly knowing and specifying what timezone that date string is in.
A Consistent Approach That Avoids All of This
Most timezone-related timestamp bugs share a root cause: timezone information entering or leaving the system implicitly. The following convention eliminates nearly all of them:
1. Store timestamps as UTC. Whether as integers (Unix seconds) or as TIMESTAMPTZ in a database, store in UTC throughout. 2. Convert to local time only at the display layer. Do the conversion as late as possible, in the UI, using the user's timezone preference. 3. Use ISO 8601 with explicit offsets for any timestamp you transmit as a string. 2024-04-25T11:46:40Z or 2024-04-25T13:46:40+02:00 — both are unambiguous. 4. Validate the scale of incoming timestamps. Reject values that would produce dates outside a reasonable range.
For one-off conversions and sanity checks, the Unix Timestamp Converter shows the UTC interpretation alongside the timestamp, which makes it easy to verify that a stored or received value corresponds to the date you expect.


