Understanding Unix Timestamps: A Developer's Guide
What Unix timestamps are, how they work, leap seconds, the 2038 problem, and practical conversion.
Why developers should understand Unix timestamps
A Unix timestamp is an integer representing the number of seconds elapsed since 00:00:00 UTC on 1 January 1970 (the Unix epoch). They are the lingua franca of time in computing: databases index them, APIs return them, file systems store them, and protocols like NTP and HTTP use them. Every developer eventually has to convert between Unix timestamps and human-readable dates β and to debug the issues that arise when different systems represent time differently.
This guide covers what Unix timestamps are, how they work in different languages, the time zone and leap second complications, and the upcoming 2038 problem.
How Unix timestamps work
A Unix timestamp is just an integer count of seconds. There is no time zone, no daylight saving, no calendar arithmetic. Adding 86,400 to a timestamp always advances the date by exactly one day in UTC.
``` 0 = 1970-01-01 00:00:00 UTC (the epoch) 1,000 = 1970-01-01 00:16:40 UTC 1,000,000 = 1970-01-12 13:46:40 UTC 1,000,000,000 = 2001-09-09 01:46:40 UTC (the "billennium") 1,500,000,000 = 2017-07-14 02:40:00 UTC 2,000,000,000 = 2033-05-18 03:33:20 UTC 2,147,483,647 = 2038-01-19 03:14:07 UTC (32-bit signed max) ```
The largest representable value depends on the integer type:
- 32-bit signed: 2,147,483,647 (2^31 β 1) β 2038-01-19 03:14:07 UTC
- 32-bit unsigned: 4,294,967,295 (2^32 β 1) β 2106-02-07 06:28:15 UTC
- 64-bit signed: 9,223,372,036,854,775,807 β year 292,277,026,596
This is why the 2038 problem is a thing: any 32-bit signed timestamp overflows on 2038-01-19. 64-bit timestamps avoid the problem for hundreds of billions of years.
Unix time vs UTC: the leap second issue
Strictly, Unix time ignores leap seconds. It counts every second as if there were exactly 86,400 per day, even when the international time authorities add a leap second to UTC. After 27 leap seconds have been inserted (as of 2024), Unix time is now about 27 seconds ahead of "true" UTC.
In practice, this rarely matters. TAI (International Atomic Time) keeps perfect seconds, and UTC stays within 0.9 seconds of UT1 (Earth's rotation time) by adding leap seconds. Unix time simply uses TAI's second count minus a fixed offset.
If you need strict atomic time, use TAI directly. For everything else, Unix time's tiny drift is irrelevant.
Time zones and offsets
A Unix timestamp refers to a single moment in time. To display it in a human-readable form, you apply a time zone offset. Common offsets:
| Zone | Abbreviation | Offset from UTC | |------|--------------|-----------------| | Coordinated Universal Time | UTC | 0 | | US Eastern (winter) | EST | β5 | | US Eastern (summer) | EDT | β4 | | US Pacific (winter) | PST | β8 | | US Pacific (summer) | PDT | β7 | | UK (winter) | GMT | 0 | | UK (summer) | BST | +1 | | Central Europe (winter) | CET | +1 | | Central Europe (summer) | CEST | +2 | | Japan | JST | +9 | | China | CST | +8 | | India | IST | +5:30 | | Australia (Sydney, winter) | AEST | +10 | | Australia (Sydney, summer) | AEDT | +11 |
Notice that India and China use a single year-round offset (no daylight saving). The US, UK, and most of Europe observe DST, which complicates everything.
A common bug: a server stores a Unix timestamp (always UTC), then displays it without applying the user's local offset, and the user sees a time that is off by several hours. Always store in UTC, always convert to local at display time.
The 2038 problem in detail
The 2038 problem (a.k.a. Y2K38) is the 32-bit signed integer overflow that will occur on 2038-01-19 03:14:07 UTC. The next second overflows to β2,147,483,648, which on most systems is interpreted as 1901-12-13 20:45:52 UTC. Anything that depends on Unix timestamps as 32-bit signed integers will break:
- File systems that store mtime in 32-bit time_t
- Older databases (older MySQL, some SQLite builds)
- Embedded systems (routers, IoT devices, cars)
- Network protocols that use 32-bit time fields (NTP, DNS, Kerberos, some TLS handshakes)
Modern systems (Linux on 64-bit, macOS on 64-bit, modern Windows, modern databases) have already migrated to 64-bit. The risk is in legacy and embedded code that has not been updated.
The fix: change the data type from `time_t` (32-bit) to `int64_t` (or equivalent). One declaration change, plus a recompile. The challenge is finding every place the 32-bit assumption lives β file formats, wire protocols, persisted data, third-party libraries.
Working with Unix timestamps in code
Python
Python's `datetime` module is the canonical tool.
```python import datetime from zoneinfo import ZoneInfo
# Current Unix timestamp (seconds, float) import time now = time.time() # 1700000000.123
# Convert Unix timestamp to datetime (UTC) ts = 1700000000 utc = datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc) # 2023-11-14 22:13:20+00:00
# Convert Unix timestamp to datetime (specific zone) ny = datetime.datetime.fromtimestamp(ts, tz=ZoneInfo("America/New_York")) # 2023-11-14 17:13:20-05:00
# Convert datetime to Unix timestamp d = datetime.datetime(2023, 11, 14, 22, 13, 20, tzinfo=datetime.timezone.utc) ts = int(d.timestamp()) # 1700000000
# Format as ISO 8601 print(utc.isoformat()) # 2023-11-14T22:13:20+00:00 ```
Watch out: `datetime.fromtimestamp(ts)` (without `tz`) uses the local time zone, which is rarely what you want in a server context. Always pass an explicit time zone.
JavaScript
JavaScript uses milliseconds, not seconds. `Date.now()` returns milliseconds since epoch.
```js // Current Unix timestamp in milliseconds const now = Date.now(); // e.g., 1700000000000
// Current Unix timestamp in seconds const nowSec = Math.floor(Date.now() / 1000);
// Convert seconds to milliseconds and create a Date const date = new Date(1700000000 * 1000); // 2023-11-14T22:13:20.000Z
// Convert milliseconds to seconds const ts = Math.floor(date.getTime() / 1000);
// Display in a specific timezone using Intl console.log(date.toLocaleString("en-US", { timeZone: "Asia/Tokyo" })); // 11/15/2023, 7:13:20 AM
// ISO 8601 console.log(date.toISOString()); // 2023-11-14T22:13:20.000Z ```
The biggest JavaScript gotcha: every other language uses seconds; JavaScript uses milliseconds. Forgetting to multiply or divide by 1000 is the source of countless "off by a factor of 1000" bugs.
SQL
Most databases support Unix timestamps natively or via functions.
```sql -- MySQL SELECT UNIX_TIMESTAMP(NOW()); -- current timestamp (seconds) SELECT FROM_UNIXTIME(1700000000); -- 2023-11-14 22:13:20 SELECT FROM_UNIXTIME(1700000000, '%Y-%m-%d %H:%i:%s');
-- PostgreSQL SELECT EXTRACT(EPOCH FROM NOW()); -- current timestamp (seconds, float) SELECT TO_TIMESTAMP(1700000000); -- 2023-11-14 22:13:20+00
-- SQLite SELECT strftime('%Y-%m-%dT%H:%M:%fZ', 'unixepoch', 1700000000); -- 2023-11-14T22:13:20.000Z ```
The MySQL `UNIX_TIMESTAMP()` returns seconds; the PostgreSQL `EXTRACT(EPOCH FROM ...)` returns seconds with fractional precision. SQLite requires the manual `strftime` recipe.
Bash
The `date` command handles Unix timestamps on every Unix system.
```bash # Current Unix timestamp date +%s # 1700000000
# Convert Unix timestamp to a date string # Linux (GNU date) date -d @1700000000 # Tue Nov 14 22:13:20 PM UTC 2023
# macOS (BSD date) date -r 1700000000 # Tue Nov 14 22:13:20 UTC 2023
# Convert a date string to a Unix timestamp # Linux date -d "2023-11-14 22:13:20 UTC" +%s # macOS date -j -f "%Y-%m-%d %H:%M:%S" "2023-11-14 22:13:20" +%s
# Display in a specific timezone TZ="Asia/Tokyo" date -d @1700000000 # Wed Nov 15 07:13:20 JST 2023 ```
For a quick reference: `date +%s` for "now as Unix time" is a one-liner you'll use weekly.
Storing timestamps in databases
Best practices for database storage:
- Store as integers (BIGINT): Native Unix timestamps are cheap to store, easy to index, simple to compare.
- Always use UTC: Convert to the local zone only at display time.
- Use BIGINT, not INT: A 32-bit signed integer overflows in 2038. Always use 64-bit.
- Consider millisecond precision: For high-frequency event logging (trading, gaming, telemetry), 1-second resolution is too coarse. Store as BIGINT milliseconds.
- Use TIMESTAMP WITH TIME ZONE: PostgreSQL's `timestamptz` is the most robust. MySQL's `TIMESTAMP` has the 2038 problem; use `DATETIME` instead.
- Index timestamp columns: Queries like `WHERE created_at > ?` are extremely common. A BTREE index on a timestamp column makes them fast.
A common schema:
```sql CREATE TABLE events ( id BIGSERIAL PRIMARY KEY, event_type TEXT NOT NULL, payload JSONB NOT NULL, created_at BIGINT NOT NULL, -- Unix ms INDEX idx_events_created (created_at) ); ```
Common pitfalls
The JavaScript millisecond bug
JavaScript timestamps are in milliseconds; every other major language uses seconds. Mixing them produces timestamps off by a factor of 1000.
```js // From a Python backend const python_ts = 1700000000; // seconds const js_date = new Date(python_ts); // 1970-01-20T08:13:20.000Z β WRONG!
// Correct: multiply by 1000 const js_date = new Date(python_ts * 1000); ```
Time zone display bugs
A timestamp refers to a moment in time, not a date. If your server logs "2023-11-14 22:13:20" without indicating UTC vs. local, the user has no way to know which it is. Always:
- Store as Unix timestamp (inherently UTC)
- Send to clients with an explicit time zone
- Convert to local at display time using the user's actual zone
```js // Server sends "2023-11-14T22:13:20Z" (Z = UTC) // Client renders it in user's local zone const d = new Date("2023-11-14T22:13:20Z"); console.log(d.toLocaleString()); // "11/14/2023, 5:13:20 PM" (in EST) ```
DST edge cases
A 24-hour day in spring is actually 23 hours (DST skips an hour); a 24-hour day in fall is 25 hours. Code that assumes "add 86,400 to get tomorrow" works fine; code that does "add 24 Γ 60 Γ 60" or "tomorrow at 2:30 AM" can fail across DST transitions.
```python # This is a 23-hour or 25-hour day in DST zones d = datetime.datetime(2024, 3, 10, 1, 0, 0, tzinfo=ZoneInfo("America/New_York")) # DST starts print((d + datetime.timedelta(days=1)).isoformat()) # 2024-03-11T01:00:00-05:00 β correct print((d + datetime.timedelta(hours=24)).isoformat()) # 2024-03-11T02:00:00-04:00 β DST has jumped, this is 25 hours later ```
For scheduling and time-of-day math, prefer libraries (Luxon, Arrow, date-fns) that handle these edge cases correctly.
Method 1: Use UtilBoxx's Timestamp Converter (Recommended)
For quick conversions during development, the UtilBoxx Timestamp Converter is a private, in-browser tool that converts between Unix timestamps and dates in any timezone, supports both seconds and milliseconds, and runs entirely client-side. No upload, no signup, no log. Bookmark it for the next time you need to decode an unfamiliar timestamp from an API response.
Conclusion
Unix timestamps are simple, fast, and universal β but the surrounding ecosystem (time zones, leap seconds, the 2038 problem, millisecond vs. second confusion) is full of traps. Every developer eventually hits each of these; the only defense is to know they exist.
Best practices:
- Store as 64-bit integers in UTC
- Convert to local at display time with the user's actual time zone
- Use a real time zone library (Luxon, date-fns-tz, zoneinfo) for non-trivial time math
- Audit 32-bit timestamps now if you maintain legacy code
Unix timestamps are one of the oldest data formats still in use, and they will outlive us all β but only if we use them correctly.