Quick answer
Milliseconds vs seconds is a unit problem, not a timezone problem.
| What you have | Modern digit count | Example | Same instant in UTC | Common systems |
|---|---|---|---|---|
| Unix seconds | 10 digits | 1700000000 |
2023-11-14T22:13:20Z |
Linux date +%s, Python time.time(), PHP time(), JWT exp |
| Unix milliseconds | 13 digits | 1700000000000 |
2023-11-14T22:13:20Z |
JavaScript Date.now(), Java currentTimeMillis(), MongoDB Date, .NET milliseconds |
| Unix microseconds | 16 digits | 1700000000000000 |
2023-11-14T22:13:20Z |
some databases, event streams, high-precision logs |
| Unix nanoseconds | 19 digits | 1700000000000000000 |
2023-11-14T22:13:20Z |
Go UnixNano(), tracing systems, low-level clocks |
The conversion is simple:
const seconds = Math.floor(milliseconds / 1000);
const backToMs = seconds * 1000;
The debugging rule is even simpler:
| Symptom | Likely mistake | Fix |
|---|---|---|
| date lands in January 1970 | seconds were used where milliseconds were expected | multiply by 1000 |
| date lands around year 55,000 | milliseconds were used where seconds were expected | divide by 1000 |
| JWT expires immediately or in decades | Date.now() was copied into a seconds claim |
use Math.floor(Date.now() / 1000) |
| cache TTL is 1000x too long or short | duration milliseconds were confused with timestamp seconds | separate ttlSeconds from expiresAtMs |
Why Unix timestamps come in both units
Classic Unix time is seconds since 1970-01-01 00:00:00 UTC. That is why shell tools, PHP, Python's basic time.time(), JWT claims, and many server APIs speak seconds by default.
JavaScript chose milliseconds for Date, and that convention spread into browser events, frontend logs, Java's System.currentTimeMillis(), MongoDB Date, and many JSON APIs written around JavaScript clients.
Neither unit is more "real." They describe the same instant at different precision:
1700000000 seconds = 2023-11-14T22:13:20Z
1700000000000 millis = 2023-11-14T22:13:20Z
1700000000000000 micros = 2023-11-14T22:13:20Z
The mistake happens at boundaries: browser to API, Java to SQL, webhook to queue, CSV import to database, or cache expiry to Redis.
The 10-digit vs 13-digit rule
For positive timestamps around today, digit count is the fastest check.
| Digits | Unit | Approximate current scale | Notes |
|---|---|---|---|
| 10 | seconds | 1.7e9 |
seconds stay 10 digits from 2001-09-09 to 2286-11-20 |
| 13 | milliseconds | 1.7e12 |
milliseconds are 13 digits over the same modern date range |
| 16 | microseconds | 1.7e15 |
divide by 1_000_000 for seconds |
| 19 | nanoseconds | 1.7e18 |
too large for exact JavaScript Number arithmetic |
Use digit count as a guardrail, not as a permanent API contract.
function toEpochMs(value) {
const n = Number(value);
if (!Number.isFinite(n)) throw new TypeError("timestamp must be numeric");
if (n < 0) throw new RangeError("negative timestamps need an explicit unit");
// Good enough for positive contemporary seconds or milliseconds.
return n < 100_000_000_000 ? n * 1000 : n;
}
That helper is intentionally conservative. It is fine for messy analytics imports and webhook cleanup jobs. It is not fine for historical datasets, pre-1970 dates, astronomy, archival systems, or anything where the upstream should have documented the unit.
Convert milliseconds to seconds
Most "milliseconds to Unix timestamp" searches mean this: "I have a 13-digit JavaScript or Java value, and the target wants 10-digit Unix seconds."
const createdAtMs = Date.now();
const createdAtUnixSeconds = Math.floor(createdAtMs / 1000);
Use integer output for API fields, database columns, JWT claims, and shell commands.
| Language | Milliseconds to seconds |
|---|---|
| JavaScript | Math.floor(ms / 1000) |
| Python | ms // 1000 |
| Java | ms / 1000L for modern positive values, Math.floorDiv(ms, 1000L) if negatives matter |
| Go | ms / 1000 with int64 |
| C# | ms / 1000L |
| PHP | intdiv($ms, 1000) |
| Shell | $((ms / 1000)) |
| PostgreSQL | floor(ms / 1000.0)::bigint |
One subtlety: negative timestamps behave differently across languages. JavaScript Math.floor(-1500 / 1000) returns -2, while Java integer division -1500 / 1000L returns -1. If your data can be before 1970, choose the rounding rule on purpose and test it.
Convert seconds to milliseconds
Use this when a seconds-based API value needs to become a JavaScript Date, Java Instant, or millisecond database field.
const createdAtSeconds = 1700000000;
const createdAtMs = createdAtSeconds * 1000;
const createdAtDate = new Date(createdAtMs);
| Language | Seconds to milliseconds |
|---|---|
| JavaScript | seconds * 1000 |
| Python | seconds * 1000 |
| Java | seconds * 1000L |
| Go | seconds * 1000 with int64 |
| C# | seconds * 1000L |
| PHP | $seconds * 1000 |
| SQL | seconds * 1000::bigint or equivalent |
Use 64-bit numbers. Current Unix seconds fit in 32 bits for now, but current Unix milliseconds already do not. This bug is easy to miss in Java, C#, SQL schemas, Protobuf messages, and older code that still uses int.
int seconds = 1700000000;
long wrong = seconds * 1000; // overflow happens before assignment
long right = seconds * 1000L; // long arithmetic
Which unit each system expects
This table is the part to keep open during code review.
| System or API | Common timestamp unit | Example |
|---|---|---|
JavaScript Date.now() |
milliseconds | 1700000000000 |
JavaScript new Date(number) |
milliseconds | new Date(1700000000 * 1000) |
Java System.currentTimeMillis() |
milliseconds | 1700000000000 |
Java Instant.getEpochSecond() |
seconds | 1700000000 |
.NET ToUnixTimeSeconds() |
seconds | 1700000000 |
.NET ToUnixTimeMilliseconds() |
milliseconds | 1700000000000 |
Python time.time() |
seconds as float | 1700000000.123 |
Python time.time_ns() |
nanoseconds | 1700000000123000000 |
Go time.Now().Unix() |
seconds | 1700000000 |
Go time.Now().UnixMilli() |
milliseconds | 1700000000000 |
Go time.Now().UnixNano() |
nanoseconds | 1700000000000000000 |
PHP time() |
seconds | 1700000000 |
Ruby Time.now.to_i |
seconds | 1700000000 |
Linux date +%s |
seconds | 1700000000 |
JWT exp, iat, nbf |
seconds | 1700000000 |
| Stripe-style API timestamp fields | seconds | created: 1700000000 |
| MongoDB BSON Date | milliseconds | stored as 64-bit milliseconds |
Fix the 1970 bug
This is the JavaScript version:
new Date(1700000000).toISOString();
// "1970-01-20T16:13:20.000Z" wrong
new Date(1700000000 * 1000).toISOString();
// "2023-11-14T22:13:20.000Z" right
It happens because new Date(number) expects milliseconds. A 10-digit seconds value is only about 1.7 billion milliseconds after 1970, which is still January 1970.
The same issue appears in frontend charts, browser logs, React state, date pickers, and analytics dashboards. If the UI shows 1970, look for a seconds value that skipped the * 1000 boundary conversion.
Fix the year-55000 bug
This is the reverse problem:
from datetime import datetime, timezone
datetime.fromtimestamp(1700000000000, tz=timezone.utc)
# ValueError or a far-future year on many systems
datetime.fromtimestamp(1700000000000 / 1000, tz=timezone.utc)
# 2023-11-14 22:13:20+00:00
Python fromtimestamp, Linux date -d @SECONDS, PHP date, many SQL epoch functions, and many API filters expect seconds. A 13-digit millisecond value becomes about 54,000 years when read as seconds.
When you see a far-future date, do not debug timezone first. Check the unit first.
JWT, tokens, and expiry fields
JWT exp, iat, and nbf use NumericDate, which is seconds since 1970-01-01T00:00:00Z UTC. This is a common production bug because JavaScript developers naturally reach for Date.now().
// Wrong: milliseconds
const exp = Date.now() + 60 * 60 * 1000;
// Right: seconds
const exp = Math.floor(Date.now() / 1000) + 60 * 60;
The same thinking applies to API fields named expires_at, created, issued_at, and not_before. If the docs show a 10-digit value, treat it as seconds unless they explicitly say otherwise.
Database and schema naming
The best long-term fix is boring names.
Use:
created_at_secondscreated_at_msexpires_at_unix_secondsclient_sent_at_msevent_time_ns
Avoid:
timestamptimecreateddateexpires
If the database column is numeric, put the unit in the column name and use a 64-bit type:
created_at_seconds BIGINT NOT NULL,
client_sent_at_ms BIGINT NOT NULL
If humans need to query ranges often, consider storing a native timestamp type as well:
created_at TIMESTAMPTZ NOT NULL,
created_at_ms BIGINT NOT NULL
That gives you readable SQL and a compact protocol value. The important part is that each column has one job.
Safe boundary patterns
Pick one unit inside a subsystem and convert only at the edge.
// API returns seconds. Frontend stores milliseconds.
type UserApi = {
id: string;
created_at: number; // Unix seconds from API
};
type UserView = {
id: string;
createdAtMs: number;
};
function fromApi(user: UserApi): UserView {
return {
id: user.id,
createdAtMs: user.created_at * 1000,
};
}
If you use TypeScript, branded types can make accidental mixing harder:
type UnixSeconds = number & { readonly unit: "seconds" };
type UnixMs = number & { readonly unit: "milliseconds" };
function secondsToMs(seconds: UnixSeconds): UnixMs {
return (seconds * 1000) as UnixMs;
}
That is not magic security. It is just friction in the right place.
Microseconds, nanoseconds, and JavaScript precision
Each unit step is a factor of 1000:
| From | To seconds | To milliseconds |
|---|---|---|
| milliseconds | ms / 1000 |
already milliseconds |
| microseconds | us / 1_000_000 |
us / 1000 |
| nanoseconds | ns / 1_000_000_000 |
ns / 1_000_000 |
The JavaScript precision trap starts at nanoseconds. A current Unix nanosecond timestamp is around 1.7e18, which is larger than Number.MAX_SAFE_INTEGER. Keep nanoseconds as BigInt or strings if exactness matters.
const ns = 1700000000000000000n;
const ms = ns / 1000000n;
If the value is only for display, converting down to milliseconds is usually fine. If it is for ordering distributed traces, audit logs, or financial events, do not silently round away precision.
A review checklist for timestamp PRs
Before merging code that moves time across a boundary, check these:
- Does the variable or field name include the unit?
- Does the receiving API document seconds, milliseconds, microseconds, or nanoseconds?
- Is a 13-digit value ever sent to a seconds-only field?
- Is a 10-digit value ever passed directly to JavaScript
Date? - Are JWT
exp,iat, andnbfvalues seconds? - Are database columns 64-bit if they store milliseconds?
- Are pre-1970 values possible, and if so, is rounding tested?
- Are nanoseconds kept out of JavaScript
Numberwhen exactness matters?
The fastest one-line code review question is: "What unit is this timestamp at this boundary?"
Official references
- MDN Date.now()
- Oracle Java System.currentTimeMillis()
- Python time.time()
- Go package time
- Microsoft DateTimeOffset.ToUnixTimeSeconds
- Microsoft DateTimeOffset.ToUnixTimeMilliseconds
- RFC 7519 NumericDate
- Stripe Charge object created timestamp
Related timestamp guides
FAQ
- Is a 13-digit timestamp seconds or milliseconds?
- For contemporary Unix timestamps, 13 digits means milliseconds. A current seconds value is around 1.7 billion and has 10 digits; the same instant in milliseconds is around 1.7 trillion and has 13 digits.
- How do I convert milliseconds to seconds?
- Divide by 1000. For JavaScript API payloads that need integer Unix seconds, use Math.floor(ms / 1000). In Python, use ms // 1000. In Java, use Math.floorDiv(ms, 1000L) if pre-1970 values are possible; otherwise ms / 1000L is fine for modern positive timestamps.
- How do I convert seconds to milliseconds?
- Multiply by 1000. Use a 64-bit type in Java, C#, Go, SQL, and Protobuf because current Unix milliseconds do not fit in a signed 32-bit integer.
- Why does my date show 1970?
- A 10-digit Unix seconds value was passed to something that expects milliseconds, such as new Date(1700000000) in JavaScript. Multiply by 1000 before creating the Date.
- Why does my date show the year 55,000?
- A 13-digit millisecond value was passed to something that expects seconds. Divide by 1000 before using Python datetime.fromtimestamp, Linux date -d @SECONDS, PHP date, or a seconds-based API field.
- Does JavaScript Date.now() return seconds or milliseconds?
- Milliseconds. Date.now() returns milliseconds since 1970-01-01 00:00:00 UTC. Use Math.floor(Date.now() / 1000) when an API expects Unix seconds.
- Are JWT exp and iat seconds or milliseconds?
- Seconds. RFC 7519 defines JWT NumericDate as seconds since 1970-01-01T00:00:00Z UTC, ignoring leap seconds. Do not put Date.now() directly into exp, iat, or nbf; divide by 1000 first.
- Should I store timestamps in seconds or milliseconds?
- Store whichever unit your system contract needs, but name it explicitly: created_at_seconds, createdAtMs, expiresAtUnixSeconds, or event_time_ms. If you control both sides and need JavaScript-friendly values, milliseconds are convenient. If you interoperate with Unix tools, JWTs, or many server APIs, seconds are often easier.
- Can I auto-detect seconds vs milliseconds?
- You can for positive contemporary values, such as treating values below 100000000000 as seconds and larger values as milliseconds. Do not rely on that for negative timestamps, historical data, far-future dates, or mixed-precision datasets.
- Are microseconds and nanoseconds also Unix timestamps?
- They can be. They use the same epoch but a smaller unit: 16 digits is usually microseconds and 19 digits is usually nanoseconds for current dates. JavaScript Number cannot safely hold current Unix nanoseconds exactly, so use BigInt or keep them as strings.