CURRENT EPOCH · EPOCHTIME.TOOLS · A PRECISION INSTRUMENT FOR TIME
Converter Batch Difference Blog
Languages
JavaScript Python TypeScript Go Rust Java PHP SQL Bash
Specialty
LDAP Timestamp .NET Ticks Chrome/WebKit Cocoa / Core Data Discord Timestamp Excel OADate Unix Hex
Standards
ISO 8601 Guide Year 2038 NTP Timestamp GPS Time Julian Day

The surprise

Here's a JavaScript snippet that, on rare occasions in production, will fail:

const start = Date.now();
doSomeWork();
const end = Date.now();
const elapsed = end - start;
console.log(`Took ${elapsed}ms`);
// → Sometimes: "Took -1832ms"

How can end be smaller than start? The work happened in real time. The function calls were sequential. What's going on?

Date.now() reads the wall clock

Date.now() returns the system wall clock — the same clock that's displayed in your operating system's status bar. That clock has at least two ways to go backwards:

  1. NTP correction. Your machine's clock drifts. The operating system periodically syncs to a time server. If your clock was running fast and got corrected, the wall clock will jump backwards by however much it was off.
  2. Manual user adjustment. Someone in Control Panel changes the time.
  3. Daylight saving time — sort of. DST changes the displayed local time, but Date.now() returns milliseconds since the Unix epoch, which is timezone-independent. So DST itself doesn't cause backward jumps. But on some operating systems with broken time handling, the DST transition can trigger a corrective NTP sync.
  4. Virtual machine resume. When a VM is suspended and resumed, its clock may snap forward or backward to match the host. Containers can exhibit similar behavior on Linux when the host clock drifts.

Any of these can cause two consecutive calls to Date.now() to return non-monotonic values. It's rare but happens often enough in production to be a real bug source.

The fix: performance.now() for durations

performance.now() reads a different clock — one that is guaranteed to be monotonic. It returns a high-resolution timestamp (sub-millisecond precision) since the page load (in browsers) or process start (in Node). It cannot go backwards.

const start = performance.now();
doSomeWork();
const end = performance.now();
const elapsed = end - start;
// elapsed is guaranteed to be >= 0

Use performance.now() for anything that measures a duration. Use Date.now() only when you need to record an absolute moment in time (a log entry, a database row, a scheduled event). Never use Date.now() for timing.

The other failure mode: ID generation

Here's a common pattern that breaks in interesting ways:

// Generate a "unique" ID using the current time
function makeId() {
  return Date.now().toString(36);
}

This works fine until two events happen in the same millisecond — then you get a duplicate. People usually add a random suffix:

function makeId() {
  return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
}

This is better, but it still has the non-monotonic problem. If your IDs are supposed to sort chronologically (a property that's often assumed without being stated), a clock-jump backwards can produce IDs that sort earlier than older IDs. If you're using IDs as cursor values in a paginated API, this is a real bug.

The fix: use UUIDv7 (or ULID, or similar). UUIDv7 explicitly handles the non-monotonic case — it remembers the last issued timestamp and ensures any new ID is at least 1ms greater, even if the wall clock went backwards. As of Node 20+, you can generate them with the crypto module; libraries exist for older Node and for browsers.

The sneakier failure: cache expiration

Here's a TTL cache implementation that looks correct but has the same bug:

class TtlCache {
  constructor(ttlMs) {
    this.ttl = ttlMs;
    this.store = new Map();
  }
  set(key, value) {
    this.store.set(key, {
      value: value,
      expiresAt: Date.now() + this.ttl
    });
  }
  get(key) {
    const entry = this.store.get(key);
    if (!entry) return undefined;
    if (Date.now() > entry.expiresAt) {
      this.store.delete(key);
      return undefined;
    }
    return entry.value;
  }
}

This works fine until the system clock jumps forward. Say it jumps forward by 1 hour due to an NTP correction. Every entry in the cache instantly expires, because Date.now() is now an hour ahead of when they were set. Your cache hit rate drops to near zero, your downstream service gets hammered, and you wonder what's happening.

The fix: again, use performance.now() for the deadline math (since it's monotonic), or store TTL as a duration rather than an absolute time and compute "now + duration" each check.

Better still: use a real cache library that handles this. There's no shame in not implementing your own TTL cache.

What you can do

Browser variance

One additional wrinkle: even performance.now() isn't perfectly precise across all browsers. To prevent timing attacks, browsers intentionally reduce its precision:

So performance.now() won't give you nanosecond precision in a normal web context. That's fine for almost all use cases — but if you genuinely need high-precision timing for benchmarks or audio sync, look at the MDN reduced time precision docs for how to enable cross-origin isolation and get sharper measurements.

For day-to-day work: use performance.now() for elapsed time, Date.now() for wall-clock moments, and forget you ever expected wall-clock time to be monotonic.


Published April 25, 2026. Tagged: javascript, time, gotchas.

← Back to blog  ·  Try the converter