Time is one of those things that looks trivial until you try to store it, compare it, or send it across a network. A surprising number of production bugs trace back to a developer treating a timestamp as a plain number when it is really a number plus a stack of assumptions. This guide walks through what a Unix timestamp actually represents, where the units trip people up, and how to convert between formats without quietly corrupting your data.

What epoch time actually is

A Unix timestamp is a single integer: the number of seconds that have elapsed since midnight UTC on January 1, 1970. That instant is called the epoch. So

text
0
is the epoch itself,
text
1000000000
is roughly September 2001, and a value like
text
1750000000
lands in mid-2025.

The key property is that a Unix timestamp is absolute and location-free. It does not carry a time zone, a date format, or a notion of "local time." It is just a count of seconds from a fixed reference point. Two machines on opposite sides of the planet that read the same instant will produce the same Unix timestamp, regardless of what their wall clocks say.

That is exactly why epoch time is the lingua franca of computing. Comparing two timestamps is just integer comparison. Computing a duration is subtraction. Sorting events is sorting numbers. None of it requires parsing or locale awareness.

One subtlety worth knowing: Unix time deliberately ignores leap seconds. A Unix day is always exactly 86,400 seconds, even though the actual astronomical day occasionally needs an extra second. This keeps the math clean at the cost of not being a perfect count of real elapsed seconds. For almost all application work this is the correct tradeoff, and you should not try to "fix" it.

Seconds vs milliseconds: the unit trap

The most common timestamp bug is a units mismatch. Unix time is canonically measured in seconds, but plenty of platforms hand you milliseconds instead:

  • C
    text
    time()
    , Python
    text
    time.time()
    (returns a float of seconds), Go
    text
    time.Now().Unix()
    , PostgreSQL
    text
    extract(epoch ...)
    seconds
  • JavaScript
    text
    Date.now()
    and
    text
    new Date().getTime()
    milliseconds
  • Java
    text
    System.currentTimeMillis()
    milliseconds

The failure mode is sneaky because both values look like plausible numbers. A seconds value interpreted as milliseconds puts you in January 1970. A milliseconds value interpreted as seconds puts you tens of thousands of years in the future.

A quick sanity check: a current seconds-based timestamp is 10 digits (until the year 2286), and a current milliseconds-based timestamp is 13 digits. If you see a 13-digit "second" or a 10-digit "millisecond," something upstream is wrong.

javascript
const secs = 1750000000; const ms = 1750000000000; // WRONG: passing seconds to a ms-based constructor new Date(secs); // 1970-01-21T... — way off // RIGHT new Date(secs * 1000); // correct instant new Date(ms); // correct instant

Write the unit into your variable names (

text
expiresAtMs
,
text
createdAtSec
) so the conversion point is obvious at every call site. If you only remember one rule from this article, make it this one. When you need to eyeball a value, a Unix timestamp converter will tell you instantly whether a number is seconds or milliseconds and what human date it maps to.

UTC vs local time

Here is the mental model that prevents most time-zone bugs:

💡

A Unix timestamp is an instant. A time zone is a lens you look at that instant through. The instant never changes; the lens changes how it reads.

The timestamp

text
1750000000
refers to one specific moment in history. Viewed through UTC it might be
text
13:46
, through
text
America/New_York
it is
text
09:46
, and through
text
Asia/Kolkata
it is
text
19:16
— all the same instant, just different wall-clock representations.

The practical consequences:

  • Store and transmit in UTC (or raw epoch). Never persist "local" times without the offset; you lose the information needed to reconstruct the instant.
  • Convert to local only at the edge — the moment you render something for a human. Do the rest of your computation in UTC.
  • A time zone is more than an offset.
    text
    Asia/Kolkata
    is
    text
    +05:30
    , but
    text
    America/New_York
    is
    text
    -05:00
    in winter and
    text
    -04:00
    in summer because of daylight saving time. Store the zone name (
    text
    America/New_York
    ), not a fixed offset, whenever future local times matter — for example, a recurring 9 a.m. meeting that must stay at 9 a.m. across a DST transition.

This is why "store the offset" is not enough for scheduling. An offset tells you the rule for one instant; a zone name carries the full ruleset, including DST changes that may not have even been legislated yet.

The year 2038 problem

The classic Unix timestamp is stored in a signed 32-bit integer. That gives a range of about ±2.1 billion seconds from the epoch. The positive ceiling is reached at 03:14:07 UTC on January 19, 2038, after which the counter overflows and wraps around to a large negative number — interpreted as December 1901.

This is the Y2K of the systems world. Any code still using a signed 32-bit

text
time_t
will misbehave as it starts doing arithmetic on dates near or past 2038 — and that arithmetic happens today whenever software computes things like a 30-year mortgage end date or a long-lived certificate expiry.

The fix is straightforward: use a 64-bit integer for time. A signed 64-bit count of seconds covers roughly 292 billion years in each direction, which comfortably outlasts any practical concern. Most modern systems have already migrated:

  • 64-bit Linux, macOS, and the major language runtimes use 64-bit time internally.
  • Embedded systems, legacy databases, old binary file formats, and network protocols with fixed-width 32-bit time fields are where the risk still lives.

If you maintain anything that packs a timestamp into exactly four bytes, that is the code to audit. The bug will not announce itself early; it appears the first time a date crosses the boundary.

ISO 8601: the human-readable companion

Raw epoch integers are perfect for machines and useless for humans. ISO 8601 is the standard string format that bridges the gap, and it is what you should use in APIs, logs, and config files. A typical value looks like this:

text
2026-06-02T14:30:00Z 2026-06-02T20:00:00+05:30

The parts:

text
YYYY-MM-DD
, a
text
T
separator,
text
HH:MM:SS
, and a zone designator. The trailing
text
Z
means UTC ("Zulu" time); otherwise an explicit offset like
text
+05:30
or
text
-04:00
is appended.

Why ISO 8601 is worth standardizing on:

  • It sorts correctly as plain text. Because the fields run largest to smallest, lexical string sorting matches chronological order. This is genuinely useful in logs and filenames.
  • It is unambiguous.
    text
    03/04/2026
    is March 4th or April 3rd depending on where you live;
    text
    2026-03-04
    is never in doubt.
  • It carries the offset. Unlike a bare epoch number, the string tells a reader the zone context explicitly.

The one rule to internalize: an ISO 8601 string without a

text
Z
or offset has no defined zone. Different parsers will guess differently — some assume UTC, some assume local — and that ambiguity is a frequent source of off-by-hours bugs. Always emit the zone designator.

Converting safely in code

The golden rule across every language: be explicit about the zone at every boundary. Bugs come from implicit conversions where a library silently applies the machine's local time zone.

Python

Use timezone-aware

text
datetime
objects, never naive ones, for anything that crosses a boundary.

python
from datetime import datetime, timezone # epoch seconds -> aware UTC datetime (explicit tz) dt = datetime.fromtimestamp(1750000000, tz=timezone.utc) # aware datetime -> epoch seconds epoch = dt.timestamp() # parse ISO 8601 (3.11+ handles the trailing Z) dt = datetime.fromisoformat("2026-06-02T14:30:00+00:00")

Avoid

text
datetime.utcnow()
and bare
text
fromtimestamp()
— both produce naive datetimes that carry no zone, which is precisely the ambiguity you are trying to eliminate. Use
text
datetime.now(timezone.utc)
instead.

JavaScript

Remember everything is in milliseconds, and a

text
Date
is internally just an epoch-ms value.

javascript
const dt = new Date(1750000000 * 1000); // seconds -> ms dt.toISOString(); // "2026-...Z" — always UTC, safe to store dt.getTime(); // epoch ms Date.parse("2026-06-02T14:30:00Z"); // -> epoch ms // localized display only — at the rendering edge dt.toLocaleString("en-US", { timeZone: "America/New_York" });

Use

text
toISOString()
for storage and
text
toLocaleString()
with an explicit
text
timeZone
only when showing a human a value. For heavy zone arithmetic, the newer built-in
text
Temporal
API removes many of the legacy
text
Date
footguns.

A repeatable checklist

  1. Decide your storage unit (epoch seconds is a fine default) and never mix it with milliseconds.
  2. Store and compute in UTC; convert to local only for display.
  3. Persist the IANA zone name (
    text
    America/New_York
    ) when future local times must survive DST.
  4. Emit ISO 8601 with an explicit
    text
    Z
    or offset in every API and log line.
  5. Use 64-bit storage for any new timestamp field.
  6. Test the boundaries: a DST transition, a leap day, and a value past 2038.

When you are debugging a stray value and just need to know what

text
1750000000
means or convert a date back into epoch form, the free Unix timestamp converter handles both directions and shows the result in UTC and your local zone side by side. It runs entirely in your browser, so the timestamps you paste in never leave your machine.

Wrapping up

Timestamps are simple once you separate the two ideas that get conflated: the instant (an absolute epoch count) and the representation (a zoned, formatted view of that instant). Keep the instant in UTC, in a known unit, in a 64-bit field. Render with an explicit zone only at the edges. Speak ISO 8601 on the wire. Do that consistently and the entire category of "why is this event showing up an hour early" bugs simply stops happening.