Unix Timestamp to Date in JavaScript, PHP, Python and MySQL

· by webmaster

A Unix timestamp is one integer that means the same instant everywhere on Earth. That makes it easy to store and compare, but converting it back into a date is where the errors start. This guide shows the conversion both ways in JavaScript, PHP, Python, MySQL and PostgreSQL. It also covers the mistakes that leave dates off by hours, off by a day, or off by a factor of 1000. The examples use 1767225600, which is 2026-01-01 00:00:00 UTC.

For a quick check while you work, the Unix Timestamp Converter translates any value to UTC and your local time, and the Date Format Converter handles the reverse.

Epoch time counts seconds since 1970 in UTC

Unix time counts seconds since 1 January 1970 00:00:00 UTC, the Unix epoch. It has no time zone. 1767225600 is the same moment in Tokyo and in New York, and zones only matter when you format the number for a person to read.

Negative values are dates before 1970. Most systems accept them, but some databases and older APIs do not.

Seconds vs milliseconds is the first bug to check

Unix, PHP, Python, MySQL and most APIs use seconds. JavaScript’s Date uses milliseconds, and so do Java, Kotlin and some Go code.

A current timestamp in seconds has 10 digits, and one in milliseconds has 13.

ValueDigitsUnitMeaning
176722560010seconds2026-01-01 00:00:00 UTC
176722560000013milliseconds2026-01-01 00:00:00 UTC
1767225600 treated as ms10wrong1970-01-21 10:53:45 UTC

If all your dates land in January 1970, you passed seconds where milliseconds were expected. If they land around the year 57,000, you did the reverse. Normalize the unit at the boundary with a small guard:

function toSeconds(n) {
  return n > 1e11 ? Math.floor(n / 1000) : n;
}

A value above 100,000,000,000 seconds would be in the year 5138, so treating a number that size as milliseconds is safe.

Unix timestamp to date in JavaScript

Timestamp to date: multiply by 1000

Date takes milliseconds, so multiply seconds by 1000.

const ts = 1767225600;
const d = new Date(ts * 1000);

d.toISOString();   // '2026-01-01T00:00:00.000Z'
d.toUTCString();   // 'Thu, 01 Jan 2026 00:00:00 GMT'
d.toString();      // local time, e.g. 'Thu Jan 01 2026 07:00:00 GMT+0700 (Western Indonesia Time)'

toISOString() always returns UTC with a Z suffix. toString() and toLocaleString() use the zone of the browser or server that runs the code.

For a specific zone or a readable format, use Intl.DateTimeFormat. It is built into every modern browser and Node, and it covers most of the formatting that developers install a date library for.

new Intl.DateTimeFormat('en-GB', {
  dateStyle: 'medium',
  timeStyle: 'short',
  timeZone: 'Asia/Jakarta',
}).format(d);
// '1 Jan 2026, 07:00'

new Intl.DateTimeFormat('en-US', {
  dateStyle: 'full',
  timeStyle: 'long',
  timeZone: 'America/New_York',
}).format(d);
// 'Wednesday, December 31, 2025 at 7:00:00 PM EST'

The same instant is New Year’s Day in Jakarta and still New Year’s Eve in New York. That one example contains the whole time-zone problem.

Date to timestamp: divide by 1000 and floor

Math.floor(Date.now() / 1000);                          // current time in seconds
Math.floor(new Date('2026-01-01T00:00:00Z').getTime() / 1000);   // 1767225600
Math.floor(Date.UTC(2026, 0, 1) / 1000);                // 1767225600 (months are 0-based)

Date.parse and new Date(string) treat an ISO string without a zone suffix as local time in modern engines, but they treat a date-only string like '2026-01-01' as UTC. Always include the Z or an offset in date strings you parse. The engine then never has to guess which of its two rules applies.

Unix timestamp to date in PHP

Timestamp to date: date() or gmdate()

date() formats in the default time zone, which date.timezone in php.ini or date_default_timezone_set() sets. gmdate() always uses UTC.

$ts = 1767225600;

echo date('Y-m-d H:i:s', $ts);    // 2026-01-01 00:00:00 (if default zone is UTC)
echo gmdate('Y-m-d H:i:s', $ts);  // 2026-01-01 00:00:00 (always UTC)
echo date('c', $ts);              // 2026-01-01T00:00:00+00:00 (ISO 8601)

For anything beyond a one-off format, use DateTimeImmutable. Prefixing the timestamp with @ creates a UTC object regardless of the default zone.

$utc = new DateTimeImmutable('@' . $ts);
echo $utc->format('Y-m-d H:i:s T');   // 2026-01-01 00:00:00 GMT+0000

$jakarta = $utc->setTimezone(new DateTimeZone('Asia/Jakarta'));
echo $jakarta->format('Y-m-d H:i:s T P');   // 2026-01-01 07:00:00 WIB +07:00

DateTime::setTimestamp() does the same on an existing object. The immutable version returns a new object from every modifier. That prevents the bug where code modifies a date in one place and another part of the code reads the changed value.

Date to timestamp: getTimestamp() ignores the zone

time();                                                 // now, in seconds
strtotime('2026-01-01 00:00:00 UTC');                   // 1767225600
(new DateTimeImmutable('2026-01-01', new DateTimeZone('UTC')))->getTimestamp(); // 1767225600
$jakarta->getTimestamp();                               // 1767225600, zone does not matter

getTimestamp() returns the same value whatever zone the object is in. The instant stays the same, and only its label changes.

Unix timestamp to date in Python

Timestamp to date: always pass tz

datetime.fromtimestamp() without a tz argument returns a naive datetime in the machine’s local zone, which is rarely what you want on a server. Pass timezone.utc.

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

ts = 1767225600

datetime.fromtimestamp(ts, tz=timezone.utc)
# datetime.datetime(2026, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)

datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
# '2026-01-01T00:00:00+00:00'

datetime.fromtimestamp(ts, tz=ZoneInfo('Asia/Jakarta'))
# datetime.datetime(2026, 1, 1, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='Asia/Jakarta'))

datetime.utcfromtimestamp() is deprecated as of Python 3.12. It returns a naive datetime that only looks like UTC, and arithmetic on it assumes local time without warning. Replace every call with fromtimestamp(ts, tz=timezone.utc). zoneinfo has been in the standard library since Python 3.9.

Millisecond values work if you divide first: datetime.fromtimestamp(ms / 1000, tz=timezone.utc).

Date to timestamp: attach tzinfo first

import time
from datetime import datetime, timezone

int(time.time())                                    # now, in seconds
int(datetime(2026, 1, 1, tzinfo=timezone.utc).timestamp())   # 1767225600
int(datetime(2026, 1, 1).timestamp())               # local zone assumed: 1767200400 in UTC+7

The last line is the trap. A naive datetime has no zone, so .timestamp() interprets it as local time, and you get a different number on every machine. Always attach tzinfo before calling .timestamp().

Unix timestamp to date in MySQL

FROM_UNIXTIME() converts seconds to a DATETIME in the session time zone. UNIX_TIMESTAMP() goes the other way.

SELECT FROM_UNIXTIME(1767225600);
-- 2026-01-01 00:00:00 (when time_zone is '+00:00')

SELECT FROM_UNIXTIME(1767225600, '%Y-%m-%d %H:%i');
-- 2026-01-01 00:00

SELECT UNIX_TIMESTAMP('2026-01-01 00:00:00');
-- 1767225600 (interpreted in the session time zone)

SELECT UNIX_TIMESTAMP();
-- current time in seconds

Both functions depend on the session zone. Check it with SELECT @@session.time_zone; and set it explicitly (SET time_zone = '+00:00';) in any connection that stores or reads timestamps. Know your column types too. MySQL stores a TIMESTAMP column as UTC and converts it on read, while it stores a DATETIME column as-is with no zone at all.

For milliseconds, divide by 1000 first. FROM_UNIXTIME() accepts a decimal argument and keeps the fractional seconds.

Unix timestamp to date in PostgreSQL

SELECT to_timestamp(1767225600);
-- 2026-01-01 00:00:00+00 (a timestamptz, shown in the session zone)

SELECT to_timestamp(1767225600) AT TIME ZONE 'Asia/Jakarta';
-- 2026-01-01 07:00:00 (a plain timestamp, without zone)

SELECT extract(epoch FROM timestamptz '2026-01-01 00:00:00+00');
-- 1767225600

SELECT extract(epoch FROM now())::bigint;
-- current time in seconds

to_timestamp() returns timestamp with time zone, which is the type you want for stored instants. extract(epoch ...) returns a double with fractional seconds, so cast to bigint for a clean integer. Watch for one trap: extract(epoch ...) on a timestamp without time zone treats the value as UTC. A column written in local time therefore comes back shifted by the local offset.

Time zones and DST: store instants, format at the end

The timestamp is unambiguous, but the date string is not. Two rules cover nearly every case:

  1. Store and transmit timestamps (or ISO 8601 strings with an offset). Convert to a zone only at the point of display.
  2. Use IANA zone names such as Europe/Berlin, never fixed offsets like +01:00, for anything involving a future date. The offset changes when daylight saving starts and ends, and the zone name follows those changes.

Daylight saving time (DST) adds two edge cases. When clocks spring forward, a local time such as 02:30 does not exist. When they fall back, 01:30 happens twice, and a naive local time cannot say which one it means. Timestamps have neither problem. Do date arithmetic on timestamps and format them only at the very end. If you schedule recurring work, remember that cron runs in the machine’s zone. The Cron Expression Generator explains the fields, but your server decides the zone.

The 2038 problem affects 32-bit timestamps

A signed 32-bit integer tops out at 2,147,483,647. As a Unix timestamp that is 2038-01-19 03:14:07 UTC. One second later it wraps to a negative number, which is 1901.

new Date(2147483647 * 1000).toISOString(); // '2038-01-19T03:14:07.000Z'
new Date(2147483648 * 1000).toISOString(); // '2038-01-19T03:14:08.000Z' (fine, JS uses doubles)

Modern 64-bit PHP, Python, Node and databases handle the integer itself. These places still break:

  • INT columns holding timestamps: use BIGINT.
  • MySQL TIMESTAMP columns: their range ends in 2038, so use DATETIME for later dates.
  • 32-bit embedded devices.
  • File formats with 32-bit time fields.

The limit already applies today. Any timestamp that reaches past 2038 now, such as a 30-year certificate expiry, hits these limits at the moment you store it.

Leap seconds are ignored by Unix time

UTC occasionally inserts a leap second to stay aligned with the Earth’s rotation. Unix time ignores them, and every day has exactly 86,400 seconds. Systems handle a leap second by repeating a timestamp or smearing it across the day, so the difference between two timestamps can be off by one second across a leap-second boundary. Very little application code is affected. If you need exact elapsed time, use a monotonic clock (performance.now(), hrtime, time.monotonic()), not wall-clock timestamps.

Unix timestamp conversion cheat sheet

TaskJavaScriptPHPPythonMySQLPostgreSQL
Now (seconds)Math.floor(Date.now()/1000)time()int(time.time())UNIX_TIMESTAMP()extract(epoch from now())::bigint
Timestamp to UTCnew Date(ts*1000).toISOString()gmdate('c', $ts)datetime.fromtimestamp(ts, tz=timezone.utc)FROM_UNIXTIME(ts) with zone +00:00to_timestamp(ts)
Timestamp to zoneIntl.DateTimeFormat with timeZone->setTimezone(new DateTimeZone(...))fromtimestamp(ts, tz=ZoneInfo(...))CONVERT_TZ(FROM_UNIXTIME(ts), '+00:00', 'Asia/Jakarta')to_timestamp(ts) AT TIME ZONE '...'
Date to timestampDate.UTC(...)/1000->getTimestamp()dt.timestamp() with tzinfoUNIX_TIMESTAMP('...')extract(epoch from ...)

When a value looks wrong, paste it into the Unix Timestamp Converter and compare the UTC result with what your code printed. A mix-up between seconds and milliseconds is the most common cause. If the JSON you are reading it from fails to parse first, see Fix: JSON.parse Unexpected Token Errors.

Tools used in this guide

Copied to clipboard!