Convert Unix Timestamp to Date Without Timezone Bugs (2026)
Converting a Unix timestamp to a date is one division operation, but the timezone you format it in is the whole bug. Here's how to get it right.
Convert a Unix Timestamp to a Date — Without the Timezone Bug
You paste 1779926400 into a converter and one tool says May 27, 2026 09:00 in Tokyo while another says May 26, 2026 17:00 in Los Angeles — same number, two different dates. The integer didn't change; only the timezone formatting it did. Converting a Unix timestamp to a date is one mental shift: the number is one instant, the formatted date is a view of that instant. Get the view right and the timezone bug disappears.
TL;DR
- A Unix timestamp is one instant — formatting it in different timezones produces different dates.
- Always know if you have seconds (10 digits) or milliseconds (13) before converting.
- In JavaScript,
new Date(secs * 1000).toISOString()is the safest default. - "Local time" in code means the runtime's clock — usually not what your user sees.
- Format with a named IANA timezone (
Asia/Tokyo), never a bare offset like+09:00.
How to Convert a Unix Timestamp to a Date
The conversion itself is trivial: multiply by 1000 if your language counts in milliseconds, divide by 1000 if it counts in seconds, then hand the number to a date constructor. The thing that fails — and fails silently — is the formatting step. A Date object in memory has no timezone; it is just a number of milliseconds since the epoch. The timezone appears the moment you ask for a string representation, and that is where the bugs come from.
Step 1: Identify seconds versus milliseconds
A 10-digit number is seconds. A 13-digit number is milliseconds. A 16-digit number is microseconds (OpenTelemetry, Python time_ns()), and 19 digits is nanoseconds (Go's UnixNano()). Anything in the 10–13 range that started life in JavaScript is almost certainly milliseconds; anything from a SQL query, a webhook payload, or date +%s is almost certainly seconds. The iKit Unix Timestamp Converter auto-detects the scale and shows the inferred unit so you don't guess wrong.
Step 2: Build a Date object, not a string
Convert to a Date (or your language's equivalent) before doing anything else. A Date has no timezone — it is an instant. Strings always have a timezone, even when it's invisible. Treat the Date as the source of truth and only render strings at the very last step, when you know who's going to read them.
// Always convert to a Date first.
const secs = 1779926400;
const d = new Date(secs * 1000); // multiply for ms
Step 3: Format with an explicit timezone
Every formatting call should name a timezone. In JavaScript, that is the timeZone option of Intl.DateTimeFormat. In Python, it is the tz argument to astimezone(). In SQL, it is AT TIME ZONE. Never assume the runtime's default zone matches what your user expects.
new Intl.DateTimeFormat('en-GB', {
timeZone: 'Asia/Tokyo',
dateStyle: 'short',
timeStyle: 'short',
}).format(d);
// → "27/05/2026, 09:00"
Why Your Unix Timestamp Shows the Wrong Date
Three classes of bug cover nearly every timezone mistake. Each one looks like the timestamp is wrong; in reality, the timestamp is fine and the formatter is misbehaving.
The 1000× problem: seconds versus milliseconds
JavaScript counts in milliseconds. Almost every other widely-used language counts in seconds. If you take a 13-digit value out of Date.now() and pass it to a Python script that expects seconds, the resulting date is roughly 55,000 years in the future. The opposite mistake — feeding seconds to new Date() without multiplying — gives you a date in January 1970, the famous "epoch zero" giveaway.
| Source | Unit | Example |
|---|---|---|
Date.now() (JS) |
milliseconds | 1779926400000 |
time.time() (Python) |
seconds (float) | 1779926400.123 |
time() (PHP) |
seconds | 1779926400 |
date +%s (Bash) |
seconds | 1779926400 |
If your timestamp came from a frontend and is going to a backend, document the unit explicitly. "We send milliseconds" is a one-line spec that prevents an entire family of bugs.
"Local time" lies
new Date(...).toString() returns the date formatted in the JavaScript runtime's local time — which means the user's browser when the code runs in the browser, and the server's /etc/timezone when it runs in Node.js. On most cloud hosts that's UTC, so a local-time string from production code looks fine until somebody runs the same code on a laptop in São Paulo and the dates shift by three hours. toISOString() is the cleaner default: it always returns UTC and tags the output with a Z.
Daylight saving boundaries
Adding 24 hours to a timestamp does not always advance one calendar day. On the day a region enters or exits DST, the same wall-clock date is 23 or 25 hours long. If you "add a day" by multiplying by 86400 seconds, you'll land on the wrong hour on roughly two days per year. The fix is to do calendar arithmetic in a timezone-aware library and only convert back to a Unix timestamp at the end.
Convert Unix Timestamp to Date in JavaScript, Python, Bash, and SQL
The mechanics differ by language but the pattern is the same: take the integer, build a date object, format with an explicit timezone.
JavaScript: Date, Intl.DateTimeFormat, and Temporal
const secs = 1779926400;
const d = new Date(secs * 1000);
// ISO 8601 in UTC — safest default
d.toISOString();
// → "2026-05-27T00:00:00.000Z"
// Formatted in a named zone
new Intl.DateTimeFormat('en-US', {
timeZone: 'America/Los_Angeles',
dateStyle: 'full',
timeStyle: 'long',
}).format(d);
// → "Tuesday, May 26, 2026 at 5:00:00 PM PDT"
The forthcoming Temporal API replaces all of this with Temporal.Instant.fromEpochSeconds(secs).toZonedDateTimeISO('Asia/Tokyo') — same idea, cleaner ergonomics. As of 2026 it ships in Firefox and is behind a flag elsewhere.
Python: datetime.fromtimestamp vs utcfromtimestamp
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
secs = 1779926400
# Aware UTC datetime — preferred
dt = datetime.fromtimestamp(secs, tz=timezone.utc)
# → 2026-05-27 00:00:00+00:00
# Same instant in Tokyo
dt.astimezone(ZoneInfo("Asia/Tokyo"))
# → 2026-05-27 09:00:00+09:00
Avoid datetime.utcfromtimestamp() — it returns a naive datetime with no tzinfo, which is the most common Python timezone footgun. Always pass tz= and you'll never accidentally mix naive and aware datetimes.
Bash: date -d @ and date -u
# UTC
date -u -d @1779926400 +'%Y-%m-%d %H:%M:%S %Z'
# → 2026-05-27 00:00:00 UTC
# Named zone via TZ
TZ='America/Los_Angeles' \
date -d @1779926400 +'%Y-%m-%d %H:%M:%S %Z'
# → 2026-05-26 17:00:00 PDT
On macOS the syntax differs — date -r 1779926400 reads the timestamp directly. Use GNU coreutils (gdate) on macOS if you want the Linux -d @ form.
SQL: Postgres and MySQL
-- Postgres
SELECT to_timestamp(1779926400)
AT TIME ZONE 'Asia/Tokyo';
-- → 2026-05-27 09:00:00
-- MySQL
SELECT CONVERT_TZ(
FROM_UNIXTIME(1779926400),
'+00:00', '+09:00'
);
-- → 2026-05-27 09:00:00
Postgres supports IANA zone names directly. MySQL needs the timezone tables loaded (mysql_tzinfo_to_sql) before you can use names; without them you're limited to fixed offsets, which are wrong on DST days (PostgreSQL date/time functions).
Common Timezone Bugs When Converting Unix Timestamps
These are the four that show up in production logs more than any others.
Logs that disagree across regions
A multi-region service logs every event in the local timezone of the server. When you correlate a payment event in us-east-1 with a fraud check in ap-northeast-1, the wall-clock times look hours apart even though the events happened seconds apart. The fix is to log Unix timestamps directly (or UTC ISO strings) in the structured field, and let the log viewer format them in the operator's chosen zone. The raw timestamp is the same everywhere — that's the whole point of using one.
A test that passes on CI and fails locally
The CI runner is UTC; your laptop is America/Chicago. A test that asserts new Date(secs * 1000).toString().startsWith('Wed May 27') passes on CI and fails at 8 PM local time because the day rolls over at midnight UTC. Lock the test's timezone with process.env.TZ = 'UTC' (Node) or use a formatter that takes an explicit timeZone option. Never assert against a string that depends on the runtime's local zone.
Mixing seconds and milliseconds inside one app
A JWT's exp claim is in seconds (RFC 7519 §4.1.4). JavaScript's Date.now() is in milliseconds. Code like if (jwt.exp < Date.now()) is wrong in two ways: the units don't match, and Date.now() is roughly 1000× larger than exp, so the comparison silently treats every token as expired. The fix is Date.now() / 1000 on the right-hand side, or pasting the token into the iKit JWT Decoder to read exp as a human date.
Date math that drifts past DST
new Date(timestamp + 7 * 86400 * 1000) looks like "add seven days," but on the DST changeover week it's actually 6 days 23 hours or 7 days 1 hour. If you're scheduling weekly recurring jobs, never compute the next run by adding fixed seconds — recalculate from the calendar in the target timezone instead.
A Conversion Workflow That Won't Break in Production
The pattern below survives every multi-region, multi-locale codebase I've worked in. Three rules, in order.
Store as UTC seconds, format at the edge
Pick one canonical representation — Unix seconds, stored as a 64-bit integer — and use it everywhere except the rendering layer. Databases, message queues, log lines, HTTP request bodies, internal function signatures: all integers, all seconds, all UTC. The formatting step happens once, in the view layer, with the user's preferred zone explicitly passed in.
Always tag display strings with a zone name
When a date appears in a UI, an email, or a CSV export, include the timezone in the string. "2026-05-27 09:00 (Asia/Tokyo)" is unambiguous; "2026-05-27 09:00" is a bug waiting for the first international user to read it. The iKit CSV ↔ JSON Converter treats timestamp columns as integers by default for exactly this reason — formatting belongs to the consumer, not the file.
Use one timestamp converter for ad-hoc debugging
Bookmark a converter that handles both seconds and milliseconds, supports IANA zone names, and runs entirely in your browser so you can paste production tokens or webhook payloads without leaking them to a remote server. Pasting exp claims or audit-log integers into a server-side "free online tool" is the same pattern as pasting a password into a search bar — it's data exfiltration with extra steps. The iKit Unix Timestamp Converter does the conversion locally in JavaScript; the integer never leaves your tab.
The whole workflow — store as integers, format at the edge, debug with a local-only converter — exists because Unix timestamps were designed as a substrate for machines, not a display format for humans. Treat them that way and the timezone bugs evaporate.
Related on iKit
- Unix Timestamp Explained: 10-Digit Numbers in Your Logs — Background on what the integer means, where it comes from, and how the digit count tells you the unit before you convert.
- Inside a JWT: A Field-by-Field Guide to Standard Claims — The
exp,iat, andnbfclaims are Unix timestamps in seconds — converting them correctly is exactly the workflow this post describes.
Related posts
How to Debug a 401 Unauthorized by Decoding the JWT (2026)
When an API returns 401 Unauthorized in 2026, the JWT is usually the smoking gun. Decode the token, read exp, aud, iss — and fix the bug in 90 seconds.
How to Debug a 400 Bad Request With URL Decoding (2026)
A 400 Bad Request usually traces to a URL decoding bug. Here's how to decode the query string, spot the broken character, and ship a fix in five minutes.
How to Decode a JWT in 2026 — Auth0 & Firebase Examples
Decode a JWT in 2026 with real Auth0 and Firebase tokens — header, payload, signature explained, common debugging traps, and why pasting tokens online is risky.