Convert a Unix Timestamp to Beijing Time (or Any Other Zone)
A log line hands you 1735689600 and you need it expressed in Beijing time. First, the definition: a Unix timestamp is the amount of time elapsed since 1970-01-01T00:00:00 UTC, and it contains no timezone at all. The same number names the same instant in Shanghai and in New York. So “convert it to Beijing time” is not a conversion between two timestamps - it is asking what the wall clock read in Beijing at that instant. The answer is 2025-01-01 08:00, a Wednesday, not midnight.
The few lines that do it
In JavaScript, hand the zone to toLocaleString and let the platform do the arithmetic:
const ts = 1735689600; // 10 digits = seconds
new Date(ts * 1000).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
// => 2025/1/1 08:00:00
Date works in milliseconds, hence the * 1000. If your value has 13 digits it is already milliseconds, and multiplying again lands you in the year 56971 - 1735689600000 read as seconds is about 55,000 years past the epoch:
const ms = 1735689600000; // 13 digits = milliseconds
new Date(ms).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
// => 2025/1/1 08:00:00 - same instant
In Python, pass the offset explicitly. Skipping the argument makes the result depend on the machine’s zone, which is exactly how two people get different answers:
from datetime import datetime, timezone, timedelta
datetime.fromtimestamp(1735689600, timezone(timedelta(hours=8)))
# datetime(2025, 1, 1, 8, 0, tzinfo=timezone(timedelta(seconds=28800)))
datetime.utcfromtimestamp() is deprecated since 3.12; use fromtimestamp with a tz. On the JVM, prefer the zone name over a bare offset:
Instant.ofEpochSecond(1735689600).atZone(ZoneId.of("Asia/Shanghai"));
// 2025-01-01T08:00+08:00[Asia/Shanghai]
From a shell:
date -d @1735689600 # this machine's zone
TZ=Asia/Shanghai date -d @1735689600 # Beijing explicitly
date -u -d @1735689600 # 2025-01-01 00:00:00 UTC
[DateTimeOffset]::FromUnixTimeSeconds(1735689600).ToOffset([TimeSpan]::FromHours(8))
# 2025/1/1 08:00:00 +08:00
The digit count nearly always gives the unit away
| Digits | Almost always | Example | Note |
|---|---|---|---|
| 10 | seconds | 1735689600 |
stays 10 digits until 2286 |
| 13 | milliseconds | 1735689600000 |
what JS, Java and Go hand you by default |
| 16 | microseconds | - | Date cannot carry them; treating them as milliseconds jumps to year 56971 |
Ecosystems disagree: Date.now(), System.currentTimeMillis() and Go’s time.Now().UnixMilli() give milliseconds, while PHP’s time(), date +%s, Go’s Unix() and MySQL’s TIMESTAMP give seconds, and PostgreSQL stores microseconds internally. Counting digits is a heuristic - it covers nearly all real data but cannot confirm what your source intended. Lengths that fit neither story (8, 9, 11, 12 digits) are refused outright by the timestamp converter, because a silently guessed unit is much harder to trace than one error message.
An eight-hour gap usually means it was shifted twice
When one person sees 00:00 and another sees 08:00 for the same digits, nobody did bad arithmetic - the value was converted on the way in and on the way out. The classic shape is a local wall-clock time written into a column that is actually UTC, then read back as UTC and converted to local again.
The convention that prevents it: store UTC, transmit UTC, convert to a local zone only when rendering. Set TZ=UTC on servers and containers instead of trusting the host default, and always include an offset in API payloads - 2025-01-01T08:00:00+08:00 and 2025-01-01T00:00:00Z are the same instant, while a bare 2025-01-01T08:00:00 delegates the interpretation to the reader. JavaScript has an extra trap here: new Date("2026-01-02") parses as UTC, but new Date("2026-01-02T00:00:00") parses as local time, so the two differ by 8 hours on a UTC+8 machine.
Beijing time has not always been UTC+8
It is today. But China observed daylight saving time from 1986 to 1991, and in those summers Beijing sat at UTC+9 - ask this machine’s zone database for 1986-05-04 and Asia/Shanghai answers GMT+09:00. Hard-coding +08:00 therefore shifts birth dates, old contracts and archive records by an hour.
So: use an offset to transmit an instant, use a zone name to interpret history. Prefer the IANA identifier Asia/Shanghai over GMT+8, UTC+8 or aliases such as PRC, which are far less portable.
When you do not want to open an editor
The timestamp converter runs both directions: paste a timestamp and get UTC plus this computer’s local reading, or type a date and time and get the timestamp back. It never feeds your input to new Date() and hopes - the fields are split and rebuilt with local getters and setters, so there is exactly one interpretation. All of it happens in the browser on this device; what you type never leaves this device.
Neighbours: days between two dates for spans, and date add and subtract for a start date plus a duration.
Common follow-up questions
How do I turn 13 digits back into seconds? Integer-divide by 1000. The millisecond remainder is lost, so keep a second field or a string if you need that precision.
Do timestamps run out in 2038? A signed 32-bit second counter stops at 2038-01-19 03:14:07 UTC (11:14:07 in Beijing), and MySQL’s TIMESTAMP has the same ceiling. Use BIGINT seconds or 13-digit milliseconds for long-lived data; JavaScript Date uses 64-bit milliseconds and is unaffected.
What about leap seconds? Unix timestamps do not count them, so an interval crossing a leap second is off by one. Irrelevant for logs and scheduling, relevant for trading and time synchronisation.
Timestamp column or datetime? Prefer a numeric timestamp or a zone-aware type such as PostgreSQL timestamptz. A plain datetime records wall-clock time with no zone and loses information the moment data crosses one.