JavaScript Countdown Gotcha: Why Date.now() Depends on the User’s Clock

TL;DR

Date.now() uses the user’s system clock, not the server’s.
If the user’s device time is wrong, your countdown timers will be wrong too.
Always treat the server as the source of truth for time-sensitive flows.

Backstory:…


This content originally appeared on DEV Community and was authored by Bruno Corrêa

TL;DR

  • Date.now() uses the user’s system clock, not the server’s.
  • If the user’s device time is wrong, your countdown timers will be wrong too.
  • Always treat the server as the source of truth for time-sensitive flows.

Backstory: A Payment Timer Gone Wrong

While integrating Nubank’s NuPay system, I built a simple countdown timer: 10 minutes (600 seconds) before the payment link expired. Straightforward, right?

Countdown timer UI

Fetch the order, grab the insertedAt timestamp, and compare it with Date.now().

Except my timer kept showing 11:50 minutes instead of 10:00.

The Gotcha: Date.now() Uses the Client Clock

After double-checking my math, I tried something unusual: I manually changed my system clock.

Suddenly the timer shifted.

That’s when I realized:

Date.now() isn’t based on server time. It’s based on the user’s system clock.

If the system time is off — even by a couple of minutes — your timer drifts. On payments, that’s a dealbreaker.

How I Fixed It

  1. Store a local timestamp on checkout
   localStorage.setItem(`nupay_order_${orderId}`, Date.now().toString())

This works reliably on the same device.

  1. Use it on the confirmation page
    Compare the stored value to Date.now() to calculate remaining time.

  2. Fallback to server data
    If the local key doesn’t exist (e.g. user switches devices), use the backend’s insertedAt.

Cross-Device Reality Check

What if a user checks out on their laptop but views the order on their phone?

  • LocalStorage works only on the same device.
  • Server timestamps cover cross-device cases.
  • Best practice: have the backend return its current time along with insertedAt so you can calculate relative to the server clock.

Polling the Order Status

To keep the order status fresh, I used polling (every 5 seconds):

  • Simpler than WebSockets/SSE
  • Stateless, reliable, easy to retry
  • Perfect for a short-lived, critical flow like payments
async function pollOrderStatus(orderId: number, timeout: number, signal: AbortSignal) {
  const startTime = Date.now()
  const interval = 5000

  while (true) {
    // stop polling if request was aborted or max timeout exceeded
    if (signal.aborted || Date.now() - startTime > timeout) break

    const { data } = await client.query({
      query: GetOrder,
      variables: { orderId },
      fetchPolicy: 'no-cache'
    })

    // Checks if order status has changed to paid/failed/canceled/etc
    if (data?.order?.status !== 'pending') {
      window.location.reload()
      break
    }

    await new Promise(r => setTimeout(r, interval))
  }
}

Key Lesson: Don’t Trust the Client Clock

  • Date.now() is only as correct as the user’s system settings.
  • Clocks can drift minutes or hours.
  • Cross-device usage makes things worse.

Always prefer:

  • Server timestamps as the source of truth
  • Local storage as a same-device helper
  • Small tolerances where exactness isn’t critical

Visualizing the Flow

User Checkout → Order Created (10 min timeout) → Store local timestamp
     ↓
Confirmation Page → Calculate Remaining Time → Show Countdown
     ↓
Start Polling (5s intervals) → Check Order Status → Update UI
     ↓
Status Change → Reload Page → Final State (Real-time feedback for user)

Conclusion

Most of us take Date.now() for granted, but in time-sensitive apps like payments, it can quietly undermine your logic.

Next time you build a countdown, remember:

The client clock is not gospel. The server is your source of truth.

References

Let’s Connect

I share real-world problem-solving and debugging lessons:
GitHub | LinkedIn | Portfolio


This content originally appeared on DEV Community and was authored by Bruno Corrêa


Print Share Comment Cite Upload Translate Updates
APA

Bruno Corrêa | Sciencx (2025-09-24T13:21:10+00:00) JavaScript Countdown Gotcha: Why Date.now() Depends on the User’s Clock. Retrieved from https://www.scien.cx/2025/09/24/javascript-countdown-gotcha-why-date-now-depends-on-the-users-clock/

MLA
" » JavaScript Countdown Gotcha: Why Date.now() Depends on the User’s Clock." Bruno Corrêa | Sciencx - Wednesday September 24, 2025, https://www.scien.cx/2025/09/24/javascript-countdown-gotcha-why-date-now-depends-on-the-users-clock/
HARVARD
Bruno Corrêa | Sciencx Wednesday September 24, 2025 » JavaScript Countdown Gotcha: Why Date.now() Depends on the User’s Clock., viewed ,<https://www.scien.cx/2025/09/24/javascript-countdown-gotcha-why-date-now-depends-on-the-users-clock/>
VANCOUVER
Bruno Corrêa | Sciencx - » JavaScript Countdown Gotcha: Why Date.now() Depends on the User’s Clock. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/09/24/javascript-countdown-gotcha-why-date-now-depends-on-the-users-clock/
CHICAGO
" » JavaScript Countdown Gotcha: Why Date.now() Depends on the User’s Clock." Bruno Corrêa | Sciencx - Accessed . https://www.scien.cx/2025/09/24/javascript-countdown-gotcha-why-date-now-depends-on-the-users-clock/
IEEE
" » JavaScript Countdown Gotcha: Why Date.now() Depends on the User’s Clock." Bruno Corrêa | Sciencx [Online]. Available: https://www.scien.cx/2025/09/24/javascript-countdown-gotcha-why-date-now-depends-on-the-users-clock/. [Accessed: ]
rf:citation
» JavaScript Countdown Gotcha: Why Date.now() Depends on the User’s Clock | Bruno Corrêa | Sciencx | https://www.scien.cx/2025/09/24/javascript-countdown-gotcha-why-date-now-depends-on-the-users-clock/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.