BytePane

Epoch Converter: Unix Timestamp to Human-Readable Date

Date & Time18 min read

The bug that puts dates in year 57,000

// JavaScript developer writes:
const expiresAt = Date.now() + 3600  // Wants: "1 hour from now in seconds"
// Date.now() → 1777334400000 (milliseconds!)
// + 3600 → 1777334403600 (still milliseconds — forgot to /1000)

// Stores in DB, sends to API...

// Python backend reads it as seconds:
from datetime import datetime, timezone
dt = datetime.fromtimestamp(1777334403600, tz=timezone.utc)
# → datetime(57346, 1, 22, ...) — Year 57,346 AD

// Or Node.js reads it back:
new Date(1777334403600)
// → 2026-04-29T01:00:03.600Z — actually correct in JS!
// (JS Date() expects ms, so it worked accidentally)

This seconds-vs-milliseconds mismatch is the #1 timestamp bug in production systems. Understanding the epoch system makes it easy to spot and fix.

Key Takeaways

  • Unix epoch = seconds since 1970-01-01T00:00:00Z UTC — 10 digits in 2026 (e.g., 1777334400)
  • JavaScript uses milliseconds — Date.now() = 13 digits. Always ÷1000 for seconds, ×1000 for display.
  • Timestamps are timezone-agnostic — they represent UTC. Convert to local time only at display.
  • Y2038 affects 32-bit systems — MySQL TIMESTAMP, embedded devices, 32-bit Linux kernels.
  • JWT tokens always use epoch seconds — milliseconds in exp/iat gives tokens valid for 55,000 years.

What is Unix epoch time?

Unix epoch time (also called Unix time, POSIX time, or a Unix timestamp) is the number of seconds elapsed since 00:00:00 UTC on Thursday, January 1, 1970, not counting leap seconds. At time of writing, the current epoch is approximately 1,777,334,400.

The choice of January 1, 1970 was pragmatic. Unix was developed at Bell Labs in the late 1960s, and the designers wanted a round date close to the system’s creation. The specific date has no special mathematical significance — it was simply convenient. POSIX.1-2017 standardized this definition, and RFC 3339 (the internet date standard) builds on it.

Unix timestamps are timezone-independent — they represent the same point in time everywhere on Earth. This makes them ideal for storing and comparing timestamps across distributed systems. The timezone complexity is pushed entirely to the display layer. According to the Stack Overflow Developer Survey 2025, date/time handling is consistently rated among the top 5 most confusing programming topics — largely because of timezone conversion errors that wouldn’t exist if timestamps were stored as Unix time.

// Current epoch seconds as of April 29, 2026 (approximate):
1777334400

// This represents the same moment everywhere:
// New York:  2026-04-29 00:00:00 EDT (UTC-4)
// London:    2026-04-29 04:00:00 BST (UTC+1)
// Tokyo:     2026-04-29 13:00:00 JST (UTC+9)
// All of these → 1777334400 seconds since epoch

Timestamp format reference: seconds vs milliseconds vs more

Different languages, APIs, and databases use different timestamp precisions. Knowing which format you’re working with is the first step to avoiding conversion bugs.

FormatExample (2026-04-29)PrecisionNotes
Unix seconds17773344001 secondPOSIX standard. 32-bit overflows 2038-01-19.
Unix milliseconds17773344000001 msJavaScript Date.now() default. 3 extra digits vs seconds.
Unix microseconds17773344000000001 μsPostgreSQL default. Many databases and system calls.
Unix nanoseconds17773344000000000001 nsGo time.UnixNano(). Int64 wraps in year 2262.
ISO 8601 (UTC)2026-04-29T00:00:00ZVariesRFC 3339 compliant. Human-readable. Not a number.
JWT NumericDate17773344001 secondRFC 7519 §2. iat/exp/nbf claims. Always seconds.

Digit-count detection rule for 2026: 10 digits = seconds, 13 digits = milliseconds, 16 digits = microseconds, 19 digits = nanoseconds. This heuristic works until year 2286 for seconds (11 digits) and year 2001 for milliseconds (already 13 digits).

Epoch conversion code: JavaScript, Python, Go, SQL

Each language has its own timestamp conventions and gotchas. The examples below all convert to/from the same moment: 1777334400 (2026-04-29T00:00:00Z).

JavaScript

Date/string → epoch

// Current epoch in seconds
const nowSeconds = Math.floor(Date.now() / 1000)
// Current epoch in milliseconds (JS native)
const nowMs = Date.now()

// Date string → epoch seconds
const epoch = Math.floor(new Date('2026-04-29T00:00:00Z').getTime() / 1000)
// → 1777334400

// Safer with explicit UTC
const d = new Date('2026-04-29')
const epochUTC = Math.floor(d.getTime() / 1000)

Epoch → human-readable

// Seconds epoch → Date
const d = new Date(1777334400 * 1000) // Must multiply by 1000!
console.log(d.toISOString()) // 2026-04-29T00:00:00.000Z

// Milliseconds epoch → Date
const dMs = new Date(1777334400000)

// Format with Intl.DateTimeFormat
const fmt = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'full',
  timeStyle: 'short',
})
console.log(fmt.format(d)) // Tuesday, April 29, 2026 at 8:00 PM
Python

Date/string → epoch

import time
from datetime import datetime, timezone

# Current epoch
now = int(time.time())  # seconds
now_ms = int(time.time() * 1000)  # milliseconds

# datetime → epoch (ALWAYS use timezone-aware datetimes)
dt = datetime(2026, 4, 29, tzinfo=timezone.utc)
epoch = int(dt.timestamp())  # 1777334400

# Naive datetime warning: datetime(2026, 4, 29).timestamp()
# uses LOCAL timezone — results differ per machine!

Epoch → human-readable

from datetime import datetime, timezone

epoch = 1777334400

# Epoch → UTC datetime
dt = datetime.fromtimestamp(epoch, tz=timezone.utc)
print(dt.isoformat())  # 2026-04-29T00:00:00+00:00

# Epoch → specific timezone
from zoneinfo import ZoneInfo  # Python 3.9+
dt_ny = datetime.fromtimestamp(epoch, tz=ZoneInfo('America/New_York'))
print(dt_ny)  # 2026-04-28 20:00:00-04:00
Go

Date/string → epoch

import "time"

// Current epoch
nowSec := time.Now().Unix()      // int64 seconds
nowNano := time.Now().UnixNano() // nanoseconds

// Time → epoch seconds
t := time.Date(2026, 4, 29, 0, 0, 0, 0, time.UTC)
epoch := t.Unix()  // 1777334400

// Parse string → epoch
layout := "2006-01-02T15:04:05Z07:00" // Go's reference time
parsed, _ := time.Parse(layout, "2026-04-29T00:00:00Z")
epoch2 := parsed.Unix()

Epoch → human-readable

import "time"

epoch := int64(1777334400)

// Epoch → time.Time (UTC)
t := time.Unix(epoch, 0).UTC()
fmt.Println(t.Format(time.RFC3339))
// 2026-04-29T00:00:00Z

// With timezone
loc, _ := time.LoadLocation("America/New_York")
tNY := time.Unix(epoch, 0).In(loc)
fmt.Println(tNY) // 2026-04-28 20:00:00 -0400 EDT
SQL

Date/string → epoch

-- PostgreSQL: timestamp → epoch
SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-04-29 00:00:00 UTC')::BIGINT;
-- 1777334400

-- MySQL/MariaDB
SELECT UNIX_TIMESTAMP('2026-04-29 00:00:00');

-- SQLite (no native timestamp type)
SELECT strftime('%s', '2026-04-29T00:00:00Z');

Epoch → human-readable

-- PostgreSQL: epoch → timestamp
SELECT to_timestamp(1777334400);
-- 2026-04-29 00:00:00+00

-- MySQL/MariaDB
SELECT FROM_UNIXTIME(1777334400);
-- 2026-04-29 00:00:00

-- SQLite
SELECT datetime(1777334400, 'unixepoch');
-- 2026-04-29 00:00:00

5 timestamp bugs you will encounter in production

Bug 1: Milliseconds passed to seconds-expecting API

// WRONG: sending ms to an API that expects seconds
fetch('/api/events', { body: JSON.stringify({ timestamp: Date.now() }) })
// → API receives 1777334400000, stores date as year 57,346

// RIGHT:
fetch('/api/events', { body: JSON.stringify({ timestamp: Math.floor(Date.now() / 1000) }) })

Bug 2: Naive datetime in Python (local timezone assumed)

# WRONG: naive datetime uses local timezone
from datetime import datetime
epoch = datetime(2026, 4, 29).timestamp()  # uses LOCAL TZ
# On a UTC server: 1777334400 ✓
# On an EST server: 1777348800 ✗ (4 hours off)

# RIGHT: always use timezone-aware datetimes
from datetime import datetime, timezone
epoch = datetime(2026, 4, 29, tzinfo=timezone.utc).timestamp()  # 1777334400 always

Bug 3: MySQL TIMESTAMP column overflow (Y2038)

-- RISKY: TIMESTAMP max is 2038-01-19 03:14:07 UTC
CREATE TABLE subscriptions (
  expires_at TIMESTAMP  -- Will REJECT values after 2038!
);

-- SAFE: Use DATETIME (no timezone but no Y2038 limit)
-- or BIGINT for Unix epoch seconds
CREATE TABLE subscriptions (
  expires_at BIGINT NOT NULL  -- Unix seconds, no Y2038 issue
);

Bug 4: Milliseconds in JWT exp/iat claims

// WRONG: exp in milliseconds — token "expires" year 57,346
const token = jwt.sign({ sub: userId, exp: Date.now() + 3600000 }, secret)

// RIGHT: RFC 7519 requires seconds
const token = jwt.sign(
  { sub: userId, exp: Math.floor(Date.now() / 1000) + 3600 },
  secret
)
// Or use jose library's expiresIn:
import { SignJWT } from 'jose'
const token = await new SignJWT({ sub: userId })
  .setExpirationTime('1h')  // jose handles seconds correctly
  .sign(secret)

Bug 5: parseInt on a float epoch

// Python time.time() returns a float with sub-second precision
import time
now = time.time()  # 1777334400.123456

# WRONG: floor vs truncation difference
int(1777334400.9)   # → 1777334400 (correct — truncates toward zero)
round(1777334400.9) # → 1777334401 (rounds UP — off by 1 second)

# RIGHT for epoch seconds: always floor (or use int() which truncates)
import math
epoch = math.floor(time.time())  # always rounds down

Online epoch converter: use it directly in your browser

BytePane’s Timestamp Converter handles both directions: paste any Unix timestamp (seconds or milliseconds — auto-detected by digit count) to get a human-readable date, or pick a date to get the epoch. All processing is client-side, no server calls, safe for JWT tokens and production timestamps.

For JWT token inspection including the iat (issued at) and exp (expiration) claims, use the JWT Decoder — it converts epoch claims to human-readable dates automatically.

The Y2038 problem: which systems are at risk

At 03:14:07 UTC on January 19, 2038, a signed 32-bit integer storing Unix seconds overflows from 2,147,483,647 to -2,147,483,648, representing December 13, 1901. The Linux kernel itself fixed this in 2020 for 32-bit architectures (MIPS, ARM) by using 64-bit time_t in kernel 5.6+. However, the application layer is another story.

SystemY2038 riskFix
MySQL TIMESTAMP columns⚠️ Max 2038-01-19 19:14:07Use DATETIME or BIGINT
MySQL DATETIME columns✅ Max 9999-12-31No action needed
32-bit embedded firmware (IoT)⚠️ Depends on time_t sizeFirmware update or replace
64-bit Linux (x86-64, ARM64)✅ Safe until year 292BNo action needed
32-bit Linux (MIPS, ARM32)✅ Fixed in kernel 5.6+Update to kernel 5.6+
JavaScript (64-bit float)✅ Safe until ±285M yearsNo action needed
Python (int is arbitrary precision)✅ No overflowNo action needed
Go int64✅ Safe until year 292BNo action needed

Frequently asked questions

What is Unix epoch time and why does it start on January 1, 1970?

Unix epoch time counts the number of seconds elapsed since 00:00:00 UTC on Thursday, January 1, 1970 (not counting leap seconds). The date was chosen by Unix developers at Bell Labs in the late 1960s as a convenient, round date near Unix's initial development. It was close enough to the present to be practical, yet far back enough not to run into overflow issues quickly with then-current hardware. The formal specification is in POSIX.1-2017 and RFC 3339. Alternative epochs exist: Windows uses January 1, 1601 (start of Gregorian calendar cycle); GPS uses January 6, 1980; Excel (incorrectly) uses January 1, 1900.

Why does my epoch converter show a date in 1970 or year 57000?

Two common bugs: (1) YEAR 1970: You're treating a milliseconds timestamp as seconds. JavaScript's Date.now() returns milliseconds (e.g., 1777334400000), but if you pass it to Python's datetime.fromtimestamp(1777334400000), Python treats it as seconds, giving a date far in the future — about year 58,000. Fix: divide by 1000 first. (2) YEAR 57000 or similar: The inverse — you have a seconds epoch but multiplied by 1000 or passed to a milliseconds-based function. JavaScript's new Date() expects milliseconds, so new Date(1777334400) gives 1970-01-21, not 2026-04-29. Fix: multiply seconds by 1000: new Date(1777334400 * 1000). The 13-digit rule: a milliseconds epoch has 13 digits in 2026 (e.g., 1777334400000). A seconds epoch has 10 digits (1777334400).

What is the Y2038 problem and does it affect me?

The Year 2038 problem (also called Y2K38 or Unix Millennium bug) occurs when systems store Unix timestamps as signed 32-bit integers. A signed 32-bit int maxes out at 2,147,483,647, which corresponds to 03:14:07 UTC on January 19, 2038. After that second, the value wraps to -2,147,483,648, representing December 13, 1901. Systems affected in 2026: embedded devices (IoT, industrial controllers, automotive), legacy 32-bit Linux kernels (MIPS, ARM), MySQL TIMESTAMP columns (which use 32-bit internally, max 2038-01-19), and some legacy programming languages. Modern 64-bit systems using int64 for timestamps are safe until year 292,277,026,596. Audit your stack: check MySQL TIMESTAMP vs DATETIME columns, embedded system firmware dates, and any 32-bit IoT devices.

How do I handle daylight saving time (DST) with Unix timestamps?

Unix timestamps are timezone-agnostic — they always represent UTC. This is their main advantage for storage and comparison. The DST complexity occurs at the display layer when converting to local time. The classic DST bug: during the "fall back" hour, the same wall clock time occurs twice (e.g., 1:30 AM EST and 1:30 AM EDT). If you store only wall clock times, you cannot distinguish them. Unix timestamps unambiguously distinguish them because they count continuous UTC seconds. In JavaScript: new Date(epoch * 1000).toLocaleDateString() uses the browser's local timezone — different developers see different dates for the same timestamp. Always store timestamps as UTC Unix seconds and convert to local time only at the display layer using Intl.DateTimeFormat with an explicit timeZone option.

What is the difference between epoch seconds and epoch milliseconds?

Epoch seconds (10 digits in 2026, e.g., 1777334400) is the POSIX/Unix standard: used by Python time.time(), Go time.Unix(), Linux system calls, JWT tokens (RFC 7519), and most databases. Epoch milliseconds (13 digits in 2026, e.g., 1777334400000) is JavaScript's native format: Date.now() and new Date().getTime() return milliseconds. Most JS APIs expect milliseconds. Epoch microseconds (16 digits) is used by PostgreSQL's timestamp internal representation and many C library functions. Epoch nanoseconds (19 digits) is Go's time.UnixNano() and Rust's Duration since UNIX_EPOCH. The detection rule for 2026: 10 digits = seconds, 13 digits = milliseconds, 16 digits = microseconds.

How do JWT tokens use epoch timestamps?

JWT tokens (RFC 7519) use NumericDate — defined as the number of seconds from 1970-01-01T00:00:00Z UTC, identical to Unix epoch seconds. Three claims use this format: iat (issued at), exp (expiration time), and nbf (not before). Critical rule: JWT epoch values are always seconds, never milliseconds. A common bug is setting exp = Date.now() (milliseconds) instead of Math.floor(Date.now() / 1000) + 3600 (seconds + 1 hour). With milliseconds, exp would be ~1.7 trillion, making the token valid for 55,000 years. Some JWT libraries silently accept oversized exp values; others reject them. Use our JWT Decoder to inspect any token's iat and exp values and verify they are in the expected range.

How should I store timestamps in a database?

Recommended pattern for 2026: store timestamps as BIGINT (int64) Unix seconds in the database, not as TIMESTAMP or DATETIME columns. Reasons: BIGINT is timezone-agnostic, has no Y2038 issue, sorts correctly as a number, and is portable across all databases. Avoid MySQL TIMESTAMP columns — they use 32-bit internally and max out at 2038-01-19 19:14:07 UTC. Use DATETIME or BIGINT instead. In PostgreSQL, TIMESTAMPTZ (timestamp with time zone) stores UTC internally and converts to the session timezone on display — a good alternative to BIGINT. For SQLite, use INTEGER (Unix epoch seconds) or TEXT (ISO 8601) since SQLite has no native date type. Add a created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())) column instead of created_at TIMESTAMP.

Convert timestamps instantly

Use BytePane’s Timestamp Converter to convert between Unix timestamps and human-readable dates. The tool auto-detects seconds vs milliseconds based on digit count, supports any timezone, and works entirely in your browser — paste a JWT token into the JWT Decoder to inspect its exp timestamp.

Open Timestamp Converter

Related articles