Skip to content
Utilities2026-06-116 min read

Timestamp Conversions Always Off by 8 Hours? A Complete Guide to Timezone Pitfalls

"The timestamp I see on the server — why is it always 8 hours off when I convert it to Beijing time?"

This question shows up in developer communities with absurd frequency. Some suspect the timestamp itself is wrong, others blame the conversion tool, but in the vast majority of cases the problem is a misunderstanding of timezones. This article lays out the relationship between Unix timestamps and timezones in one place — and if you just want to check what a timestamp means across zones, a timestamp converter does it instantly in the browser.

Unix Timestamps Are Always UTC — This Is the Premise for Everything

The definition of a Unix timestamp: the number of seconds (or milliseconds) elapsed from 1970-01-01 00:00:00 UTC to the target moment.

Note the keyword: UTC. A timestamp carries no timezone information — it represents an absolute point in time. The timestamp 1721900000 is late afternoon on July 25, 2024 in Beijing, early morning the same day in New York, and mid-morning in London — but the numeric value is always the same (verify it yourself with a timestamp converter).

The so-called "8-hour offset" boils down to:

  • China uses UTC+8 (East 8th timezone)
  • When you treat a UTC timestamp "as local time" for display, or conversely generate a timestamp by treating local time "as UTC," you get exactly an 8-hour skew
// A Unix timestamp (seconds)
const ts = 1721900000;

// Correct: JavaScript Date displays in the local timezone automatically
const date = new Date(ts * 1000); // JS uses milliseconds
console.log(date.toString());
// In a UTC+8 environment: "Thu Jul 25 2024 17:33:20 GMT+0800"
// In a UTC environment:   "Thu Jul 25 2024 09:33:20 GMT+0000"

// Same timestamp, different machines show different times — but the timestamp itself hasn't changed

Seconds, milliseconds, microseconds, nanoseconds

Before you blame timezones, confirm the unit. Digit length is a practical heuristic:

Digits (approx.)UnitTypical producers
10secondsUnix, Redis, many SQL columns
13millisecondsJavaScript Date.now(), Java, Go millis
16microsecondssome DB / telemetry stacks
19nanosecondsGo/Java nano clocks

Misreading a 13-digit value as seconds lands you in the year 50,000-ish; misreading a 19-digit nano value as seconds is even worse. ToolVault’s converter auto-detects s / ms / µs / ns (including 19-digit nanos) and shows local, UTC, ISO, and relative forms. If two systems disagree by exactly 1000×, it is usually a unit bug, not a timezone bug.

Three Most Common "Off by 8 Hours" Scenarios

Scenario 1: Server Formats in UTC, Frontend Displays Directly

Many backend services (especially cloud-deployed ones) default to UTC. If the backend returns a formatted string instead of a timestamp:

# Python backend (server timezone is UTC)
from datetime import datetime
now = datetime.utcnow()
print(now.strftime('%Y-%m-%d %H:%M:%S'))
# Output: 2024-07-25 09:33:20 (UTC time)

The frontend receives "2024-07-25 09:33:20" and displays it directly — Chinese users see a time "8 hours short."

Fix: Have the backend always return timestamps or ISO 8601 format (with timezone identifier), and let the frontend handle localized display:

# Recommended: return ISO format with timezone
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
print(now.isoformat())
# "2024-07-25T09:33:20+00:00" —— the frontend can parse this correctly

Scenario 2: JavaScript new Date() Parsing a String Without Timezone

// This string has no timezone info
const str = '2024-07-25 09:33:20';

// Most browsers parse it as local time (UTC+8)
const d1 = new Date(str);
console.log(d1.getTime() / 1000); // The resulting timestamp corresponds to UTC 01:33:20

// If the backend considers this UTC, you're 8 hours off
// Correct approach: specify the timezone explicitly
const d2 = new Date('2024-07-25T09:33:20Z'); // Z means UTC
const d3 = new Date('2024-07-25T09:33:20+08:00'); // explicitly East 8th

Browser differences on space-separated forms are real; prefer the T separator and an explicit offset or Z.

Scenario 3: Database Storage and Read Timezone Mismatch

MySQL's TIMESTAMP type converts based on the connection's time_zone variable, while DATETIME does no conversion. If your app server is in UTC but the database is configured as +08:00, writes and reads can drift by 8 hours.

-- Check the current session timezone
SELECT @@session.time_zone;

-- Set it to UTC uniformly
SET time_zone = '+00:00';

DST and half-hour zones (not every offset is eight)

"Always eight hours" is a China-centric meme. Regions with daylight saving time jump by one hour twice a year; zones like Asia/Kolkata (UTC+5:30) or Australia/Eucla (UTC+8:45) break naive "add a fixed offset" code. Prefer IANA zone IDs (Asia/Shanghai) over hard-coded +08:00 when you need civil time for a location across seasons.

Timezone Handling Notes per Language/Library

EnvironmentWhat to Watch For
JavaScript DateInternally stores UTC ms; toString() outputs in local timezone, toISOString() outputs UTC
dayjsUses local timezone by default; needs dayjs.tz plugin for other zones
momentmoment() is local, moment.utc() is UTC, moment.tz() specifies a zone
Python datetimenaive datetime has no timezone; always use datetime.now(timezone.utc)
Java InstantInstant.now() is always UTC; convert to local with ZonedDateTime
Go timetime.Now() carries local Location; time.Unix() returns local-zone time
// dayjs handling timezone conversion correctly
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';

dayjs.extend(utc);
dayjs.extend(timezone);

const ts = 1721900000;

// Timestamp → Beijing time
const beijing = dayjs.unix(ts).tz('Asia/Shanghai');
console.log(beijing.format('YYYY-MM-DD HH:mm:ss'));
// "2024-07-25 17:33:20"

// Timestamp → UTC
const utcTime = dayjs.unix(ts).utc();
console.log(utcTime.format('YYYY-MM-DD HH:mm:ss'));
// "2024-07-25 09:33:20"

Practical habits that prevent timezone bugs

  1. Standardize on UTC for storage and transport: Store UTC in the database; return UTC timestamps or ISO strings with a Z suffix from APIs.
  2. Only convert timezones at the presentation layer: The frontend converts display based on the user's timezone (Intl.DateTimeFormat().resolvedOptions().timeZone).
  3. Never assume the server timezone: Set it explicitly at app startup rather than relying on the system default.
  4. Avoid naive datetime: Every datetime object should carry timezone information.
  5. Label the unit in API docs (epoch_ms vs epoch_s) so clients do not guess from digit length alone.

Using the on-site converter honestly

The Timestamp Converter supports unit auto-detection, multi-timezone comparison via Intl, batch lines of timestamps, and relative phrasing. All math runs locally in your browser — useful when production values must not leave your machine. Boundaries: it does not replace a library’s DST rule database for historical civil times far from “now,” and it will not fix a backend that already stored the wrong absolute instant. Use it to inspect and compare; fix storage and contracts at the source.

Related reading: the fuller timestamp converter guide covers UI workflows and language snippets; this article focuses on the timezone failure modes that make those conversions look “wrong.”

Implementation notes

The confidence behind 'Using the on-site converter honestly' comes from a real crash: automatic unit detection buckets by digit count, and a 19-digit nanosecond timestamp (the kind Go and Java produce) was once classified as seconds, landing the date in the year 50000 CE. The nanosecond bucket was added afterwards — logic that 'looks like it cannot be wrong', such as unit detection, is exactly the kind that deserves testing at extreme digit counts.


Provided by ToolVault. Related tools: Timestamp Converter, Cron Parser. See the homepage for more developer tools.