Unix Timestamps Explained: Seconds, Milliseconds, and the Year 2038

Unix time is one number counting seconds since 1970. I cover seconds vs milliseconds, the 2038 problem, and the timestamp bug that cost me an afternoon.

One number to rule all clocks

Unix time is the simplest idea in computing that still confuses people daily: a single integer counting the seconds since midnight UTC on January 1, 1970. That starting point is called the epoch. Right now, as I write this, the counter reads 1786406400, which decodes to exactly midnight UTC on August 11, 2026. No months, no time zones, no daylight saving, just one number ticking up forever at one per second.

The beauty is that arithmetic works. Want to know what is 90 minutes from now? Add 5400. Want the difference between two events? Subtract. Compare two timestamps from servers on opposite sides of the planet and the bigger number happened later, full stop. Every date library, log file, database, and API is ultimately shuffling this integer around and only dressing it up as a human date at the last possible moment.

Seconds vs milliseconds: the ten-versus-thirteen digit rule

Here is where the wheels come off in practice. Unix classically counts seconds, but JavaScript's Date, per the MDN documentation, counts milliseconds since the same epoch. Java does milliseconds too. Python's time.time() returns seconds as a float. So the same instant is 1786406400 in one system and 1786406400000 in another, and both numbers will happily flow through your code without complaint.

The survival trick is digit counting. A seconds timestamp for any date between September 2001 and the year 2286 has exactly 10 digits. A milliseconds timestamp for the current era has 13. See a 13-digit number where you expected seconds, or a 10-digit one where you expected milliseconds, and you have found your bug. The failure modes are memorably weird: feed a seconds value like 1786406400 into a JavaScript Date, which expects milliseconds, and you get January 21, 1970, because 1.79 billion milliseconds is only about 20.7 days. Feed the milliseconds value into a seconds-based API and you land somewhere past the year 58,000.

The afternoon I lost to a factor of 1000

My own confession: I once spent an entire afternoon debugging a scheduled-jobs system where every task appeared to have run 56,000 years in the future. One service wrote timestamps from JavaScript, in milliseconds. The consuming service was Python, expecting seconds. Nothing crashed, no exception fired, every row inserted cleanly. The data was simply nonsense, and it took me hours to think of dividing one sample value by 1000 and recognizing yesterday's date staring back at me.

The fix took five minutes. The lesson took longer to sink in: timestamp unit mismatches are silent. Type systems do not catch them because both units are just integers. Tests do not catch them unless you assert on absolute values. Ever since, whenever I see a suspicious timestamp in a log or an API response, I paste it into an epoch timestamp converter before forming any theory at all. Thirty seconds of checking beats three hours of confident debugging in the wrong direction.

Time zones are display-only

This is the mental model that unlocks everything: a Unix timestamp has no time zone. It cannot, because it is a count of seconds since a fixed instant, and that instant is the same everywhere in the universe. The timestamp 1786406400 is midnight in London, 8:00 PM on August 10 in New York, and 10:00 AM on August 11 in Sydney, yet all three are the same moment and the same number. Time zones only enter the picture when a human needs to read the value, at which point software formats the instant for a particular location.

The practical rule that falls out of this: store and transmit timestamps in UTC, convert to local time only at the edge, in the user's browser or app. Every system I have seen that stored local times eventually produced double-booked meetings or off-by-one-day reports the week the clocks changed. When I need to sanity-check what an instant looks like across offices, a time zone converter settles it faster than mental arithmetic, and I wrote about the human side of this problem in my post on scheduling across remote teams.

The 2038 problem, briefly and honestly

Old systems stored Unix time in a signed 32-bit integer, whose maximum value is 2,147,483,647. The counter hits that ceiling at 03:14:07 UTC on January 19, 2038, and one second later a naive 32-bit system wraps around to a negative number, which decodes to December 1901. That is the year 2038 problem, the millennium bug's quieter sibling.

How worried should you be? For mainstream platforms, barely: modern operating systems, databases, and languages moved to 64-bit time long ago, and a 64-bit counter lasts roughly 292 billion years. The genuine risk lives in embedded systems, old file formats, and long-lived binary protocols that froze a 32-bit field decades ago and will still be running in 2038. If you maintain anything with a 32-bit time field, the deadline is real and it is closer than it feels: under twelve years away as I write this.

Where timestamps hide in your stack

Once you know the shape, you see the number everywhere. JWT tokens carry their expiry as a Unix seconds value in the exp claim, which is why a JWT decoder shows you a raw 10-digit number next to the human date; I unpacked that whole format in my JWT explainer. HTTP caching headers, OAuth token responses, webhook signatures, and database migration files all lean on epoch time.

Even identifiers sneak time in. UUID version 7, the modern default for database keys, embeds a millisecond Unix timestamp in its first 48 bits so that IDs sort by creation time; generate a few with a UUID generator and the leading characters barely change from one to the next, because the clock bits dominate. Twitter-style snowflake IDs do the same trick. Recognizing embedded epochs turns a lot of opaque strings into readable ones.

The rules I actually follow now

After enough scar tissue, my timestamp discipline compresses to a short list, and none of it is clever. Every line exists because its absence once cost somebody an afternoon, frequently mine. It is all about refusing to let ambiguity survive past a function boundary.

  • Store UTC everywhere; convert to local time only in the UI
  • Name fields with their unit: created_at_ms or expires_at_s, never just timestamp
  • Count digits before debugging: 10 is seconds, 13 is milliseconds
  • Decode any suspicious value in a converter before theorizing
  • Treat any date before 2001 or after 2100 in fresh data as a unit bug until proven otherwise
  • Audit 32-bit time fields now, not in 2037

Questions people ask

What exactly is the Unix epoch?

Midnight UTC on January 1, 1970. Unix time is the number of seconds elapsed since that instant, ignoring leap seconds, so every timestamp is a single integer that means the same moment everywhere on Earth.

How do I tell seconds from milliseconds at a glance?

Count digits. For dates in the current era, a seconds timestamp has 10 digits and a milliseconds timestamp has 13. A JavaScript Date wants milliseconds, so a 10-digit value fed to it lands you back in January 1970.

Do Unix timestamps change with daylight saving time?

No. The counter ticks uniformly in UTC and has no concept of zones or DST. Only the formatted, human-readable rendering of a timestamp shifts when clocks change, which is exactly why you should store the raw value.

Will the year 2038 problem actually break things?

Mainstream 64-bit systems are fine for billions of years. The risk is concentrated in embedded devices, legacy file formats, and old protocols that fixed a signed 32-bit field, which overflows at 03:14:07 UTC on January 19, 2038.

Read next

All articles