Convert a Unix Timestamp to Beijing Time (or Any Other Zone)

· 5 min read · timestamps timezones · 简体中文

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.

Support on PayPal