Unix Timestamp Guide: Epoch Time Explained for Developers
Every time you call Date.now() in JavaScript, time.time() in Python, or System.currentTimeMillis() in Java, you are working with Unix timestamps. This deceptively simple concept, counting seconds since a fixed point in time, underlies virtually every time-related operation in computing: database records, API responses, log files, cron jobs, JWT expiration, and file modification dates.
Yet timestamps are a consistent source of bugs. Timezone mismatches, seconds vs. milliseconds confusion, and the looming Y2K38 problem have caused countless production incidents. This guide explains exactly how Unix timestamps work, how to convert between formats in every major language, and how to avoid the pitfalls that trip up even experienced developers.
1. What Is a Unix Timestamp?
A Unix timestamp (also called epoch time, POSIX time, or Unix time) is the number of seconds that have elapsed since January 1, 1970, 00:00:00 UTC. This date is called the "Unix epoch" or simply "the epoch."
For example, the timestamp 1740268800 represents February 23, 2025, 00:00:00 UTC. The timestamp 0 represents the epoch itself: midnight on January 1, 1970. Negative timestamps represent dates before the epoch: -86400 is December 31, 1969.
| Timestamp | Human-Readable Date (UTC) |
|---|---|
| 0 | January 1, 1970 00:00:00 |
| 86400 | January 2, 1970 00:00:00 |
| 946684800 | January 1, 2000 00:00:00 |
| 1000000000 | September 9, 2001 01:46:40 |
| 1609459200 | January 1, 2021 00:00:00 |
| 1769472000 | January 1, 2026 00:00:00 |
| 2000000000 | May 18, 2033 03:33:20 |
| 2147483647 | January 19, 2038 03:14:07 (Y2K38 limit) |
The key insight is that a Unix timestamp is always in UTC. It does not have a timezone. The timestamp 1769472000 means exactly the same thing regardless of whether you are in New York, Tokyo, or London. The timezone only matters when you display the timestamp as a human-readable date.
2. Why Timestamps Exist
Storing dates as a single integer has significant advantages over storing them as formatted strings like "February 23, 2025":
- Language and locale independent: The timestamp
1740268800is the same in every country, language, and date format convention. No ambiguity about whether "02/03/2025" means February 3rd (US) or March 2nd (Europe). - Trivial comparison: Is date A before date B? Just compare two integers.
if (timestampA < timestampB)is faster and simpler than parsing and comparing date strings. - Easy arithmetic: "What time is it 24 hours from now?" Add 86400. "How many days between two dates?" Subtract and divide by 86400. No need to worry about month lengths or leap years.
- Compact storage: A 32-bit integer takes 4 bytes. The string "2025-02-23T00:00:00Z" takes 20 bytes. In databases with millions of rows, this difference matters.
- Timezone neutral: Timestamps are inherently UTC. This eliminates an entire class of bugs related to timezone conversion and daylight saving time.
3. Converting Between Formats
Timestamp to Human-Readable Date
The most common conversion. Given a timestamp, produce a date string that humans can read. The key decision is which timezone to display in.
// JavaScript: timestamp to readable date
const timestamp = 1769472000;
const date = new Date(timestamp * 1000); // JS uses milliseconds
console.log(date.toUTCString());
// "Wed, 01 Jan 2026 00:00:00 GMT"
console.log(date.toISOString());
// "2026-01-01T00:00:00.000Z"
console.log(date.toLocaleDateString('en-US', {
weekday: 'long', year: 'numeric',
month: 'long', day: 'numeric',
timeZone: 'America/New_York'
}));
// "Wednesday, December 31, 2025" (EST is UTC-5)
Date String to Timestamp
The reverse conversion. Given a human-readable date, produce a Unix timestamp.
// JavaScript: date string to timestamp
const dateStr = '2026-01-01T00:00:00Z';
const timestamp = Math.floor(new Date(dateStr).getTime() / 1000);
console.log(timestamp); // 1769472000
// Be careful: without the Z, JavaScript may interpret
// the string in local time, not UTC!
const ambiguous = new Date('2026-01-01T00:00:00'); // Local time!
const explicit = new Date('2026-01-01T00:00:00Z'); // UTC!
Convert Timestamps Instantly
Timestamp Forge converts between Unix timestamps, ISO 8601, and human-readable dates in real time. No signup required.
Open Timestamp Forge4. Timestamps in Every Language
JavaScript
// Current timestamp (seconds)
const now = Math.floor(Date.now() / 1000);
// Current timestamp (milliseconds) - native JS precision
const nowMs = Date.now();
// Timestamp to Date object
const date = new Date(timestamp * 1000);
// Date to timestamp
const ts = Math.floor(date.getTime() / 1000);
Python
import time
from datetime import datetime, timezone
# Current timestamp (float with subsecond precision)
now = time.time() # e.g., 1769472000.123456
# Current timestamp (integer)
now_int = int(time.time())
# Timestamp to datetime (UTC)
dt = datetime.fromtimestamp(1769472000, tz=timezone.utc)
# datetime(2026, 1, 1, 0, 0, tzinfo=timezone.utc)
# Datetime to timestamp
ts = int(dt.timestamp())
# Formatted string
formatted = dt.strftime('%Y-%m-%d %H:%M:%S UTC')
# '2026-01-01 00:00:00 UTC'
PHP
// Current timestamp
$now = time();
// Timestamp to date string
$date = date('Y-m-d H:i:s', 1769472000);
// '2026-01-01 00:00:00'
// Date string to timestamp
$ts = strtotime('2026-01-01 00:00:00 UTC');
// DateTime object
$dt = new DateTime('@1769472000');
$dt->setTimezone(new DateTimeZone('UTC'));
Java
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.LocalDateTime;
// Current timestamp (seconds)
long now = Instant.now().getEpochSecond();
// Timestamp to Instant
Instant instant = Instant.ofEpochSecond(1769472000L);
// Instant to LocalDateTime in UTC
LocalDateTime dt = LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
// LocalDateTime to timestamp
long ts = dt.toEpochSecond(ZoneOffset.UTC);
Go
package main
import (
"fmt"
"time"
)
func main() {
// Current timestamp
now := time.Now().Unix()
// Timestamp to Time
t := time.Unix(1769472000, 0).UTC()
fmt.Println(t) // 2026-01-01 00:00:00 +0000 UTC
// Time to timestamp
ts := t.Unix()
// Formatted string
formatted := t.Format("2006-01-02 15:04:05")
}
Bash / Command Line
# Current timestamp
date +%s
# Timestamp to human readable (GNU date)
date -d @1769472000
# Thu Jan 1 00:00:00 UTC 2026
# Timestamp to human readable (macOS date)
date -r 1769472000
# Human readable to timestamp (GNU date)
date -d "2026-01-01 00:00:00 UTC" +%s
SQL
-- PostgreSQL
SELECT to_timestamp(1769472000);
SELECT EXTRACT(EPOCH FROM NOW());
SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-01-01 00:00:00 UTC');
-- MySQL
SELECT FROM_UNIXTIME(1769472000);
SELECT UNIX_TIMESTAMP();
SELECT UNIX_TIMESTAMP('2026-01-01 00:00:00');
-- SQLite
SELECT datetime(1769472000, 'unixepoch');
SELECT strftime('%s', 'now');
SELECT strftime('%s', '2026-01-01 00:00:00');
5. The Timezone Problem
Timezones are the single greatest source of timestamp-related bugs. The root cause is that a Unix timestamp is always UTC, but humans think in local time. Every conversion between the two is an opportunity for error.
The Offset Trap
UTC offsets change throughout the year due to Daylight Saving Time (DST). New York is UTC-5 in winter and UTC-4 in summer. This means you cannot simply subtract 5 hours from UTC to get New York time, because sometimes you need to subtract 4. The only reliable way to handle this is using timezone databases (like IANA/Olson tz database) that track historical and future DST rules.
// WRONG: hard-coding UTC offset
const nyTime = new Date(timestamp * 1000 - 5 * 3600 * 1000);
// RIGHT: using timezone-aware formatting
const nyTime = new Date(timestamp * 1000).toLocaleString('en-US', {
timeZone: 'America/New_York'
});
Store UTC, Display Local
The golden rule of timestamp handling: always store timestamps in UTC (or as Unix timestamps, which are inherently UTC), and convert to the user's local timezone only at display time. Never store local times in your database.
The ISO 8601 Standard
When you need to exchange timestamps as strings (in APIs, JSON, logs), use ISO 8601 format: 2026-01-01T00:00:00Z. The trailing Z means UTC. You can also specify an offset: 2026-01-01T05:30:00+05:30 (India Standard Time). ISO 8601 is unambiguous, sortable, and universally understood.
6. Seconds vs. Milliseconds vs. Microseconds
Different systems use different units for timestamps, and confusing them is a common source of bugs:
| Unit | Used By | Example for 2026-01-01 |
|---|---|---|
| Seconds | Unix/POSIX, Python, PHP, most APIs | 1769472000 |
| Milliseconds | JavaScript, Java, Firestore | 1769472000000 |
| Microseconds | PostgreSQL, Python datetime | 1769472000000000 |
| Nanoseconds | Go, InfluxDB, Prometheus | 1769472000000000000 |
A simple heuristic for identifying the unit: count the digits. 10 digits is seconds. 13 digits is milliseconds. 16 digits is microseconds. 19 digits is nanoseconds.
// Detecting timestamp precision
function detectPrecision(timestamp) {
const digits = String(Math.abs(timestamp)).length;
if (digits <= 10) return 'seconds';
if (digits <= 13) return 'milliseconds';
if (digits <= 16) return 'microseconds';
return 'nanoseconds';
}
// Converting to seconds (the universal format)
function toSeconds(timestamp) {
const digits = String(Math.abs(timestamp)).length;
if (digits <= 10) return timestamp;
if (digits <= 13) return Math.floor(timestamp / 1000);
if (digits <= 16) return Math.floor(timestamp / 1000000);
return Math.floor(timestamp / 1000000000);
}
7. The Year 2038 Problem (Y2K38)
On January 19, 2038, at 03:14:07 UTC, Unix timestamps stored as signed 32-bit integers will overflow. The maximum value of a signed 32-bit integer is 2,147,483,647, which corresponds to that exact date and time. One second later, the counter wraps around to -2,147,483,648, which the system interprets as December 13, 1901.
This is not a theoretical concern. It has already caused real problems:
- In 2006, AOL's billing system began sending customers bills dated in 2038, causing widespread confusion.
- Embedded systems with long lifespans (industrial controllers, automotive computers, medical devices) manufactured today may still be running in 2038.
- Any 32-bit system that needs to represent dates beyond 2038 is already affected. Certificate expiration dates, loan maturity dates, and long-term scheduling are all vulnerable.
The Solution
Use 64-bit timestamps. A signed 64-bit integer can represent dates until approximately 292 billion years from now, which should suffice. Most modern operating systems and programming languages have already migrated:
- Linux: The kernel uses 64-bit time on all architectures since Linux 5.6 (2020).
- JavaScript: Uses 64-bit floating-point numbers for
Date.getTime(), so it is not affected. - Python: Arbitrary-precision integers, so it is not affected.
- Java:
java.time.Instantuses 64-bit long, good until year 1,000,000,000. - MySQL: The
TIMESTAMPtype is 32-bit and will overflow. UseDATETIMEorBIGINTinstead. - Embedded C: Compile with
-D_TIME_BITS=64on 32-bit platforms (glibc 2.34+).
-- MySQL: TIMESTAMP has Y2K38 problem, DATETIME does not
-- BAD: overflows in 2038
ALTER TABLE events ADD created_at TIMESTAMP;
-- GOOD: safe until year 9999
ALTER TABLE events ADD created_at DATETIME;
-- ALSO GOOD: store as 64-bit integer
ALTER TABLE events ADD created_at BIGINT;
8. Common Gotchas
JavaScript's Millisecond Timestamps
JavaScript's Date object works in milliseconds, but most APIs return timestamps in seconds. Forgetting to multiply or divide by 1000 is the most common timestamp bug in JavaScript.
// BUG: treats seconds as milliseconds
const date = new Date(1769472000);
// Result: January 21, 1970 (wrong!)
// FIX: multiply by 1000
const date = new Date(1769472000 * 1000);
// Result: January 1, 2026 (correct!)
Floating-Point Precision
Python's time.time() returns a float, which can lose precision for timestamps with microsecond components. For precise timing, use time.time_ns() (Python 3.7+) which returns nanoseconds as an integer.
Leap Seconds
Unix time does not count leap seconds. When a leap second occurs, Unix time either repeats a second or skips one. This means Unix timestamps are not perfectly aligned with TAI (International Atomic Time). In practice, this rarely matters unless you are working on time-critical scientific or financial systems.
Date Parsing Without Timezone
Parsing a date string without an explicit timezone is dangerous because the result depends on the system's local timezone, which varies between servers, user devices, and even between browser tabs.
// DANGEROUS: no timezone specified, behavior is implementation-defined
new Date('2026-01-01 00:00:00')
// SAFE: explicit UTC
new Date('2026-01-01T00:00:00Z')
// SAFE: explicit offset
new Date('2026-01-01T00:00:00+00:00')
9. Best Practices
- Always store times as UTC timestamps or ISO 8601 strings with timezone. Never store local times without timezone information.
- Use 64-bit integers for timestamps. Even if Y2K38 is 12 years away, any system that handles future dates (subscriptions, contracts, certificates) needs 64-bit now.
- Be explicit about precision. Document whether your API returns seconds, milliseconds, or microseconds. Name fields clearly:
created_at_msis better thancreated_at. - Use ISO 8601 for human-readable serialization.
2026-01-01T00:00:00Zis universally understood. Avoid ambiguous formats like01/02/2026. - Never calculate timezone offsets manually. Use timezone libraries (Intl API in JS, pytz/zoneinfo in Python, java.time in Java). DST rules change frequently and are country-specific.
- Test with edge cases. Test with timestamps 0, -1, 2147483647, and 2147483648 (Y2K38 boundary). Test with dates near DST transitions (March/November for US timezones).
- Use monotonic clocks for measuring durations.
Date.now()andtime.time()can jump forward or backward when the system clock is adjusted. Useperformance.now()(JS) ortime.monotonic()(Python) for measuring elapsed time.
Convert Any Timestamp Format
Timestamp Forge handles seconds, milliseconds, ISO 8601, and human-readable dates. Convert between any format instantly.
Open Timestamp Forge