cloudflare / cloudflare/workerd
Date.now() and process.hrtime() return wall clock at top-level (global scope) in wrangler dev / Miniflare, but 0 in deployed workerd
- Dominant language
- C++
- Stars
- 8.7k
- Forks
- 739
- Avg merge
- 2d 20h
- Merged PRs (30d)
- 174
Description
### What versions & operating system are you using?
System:
OS: macOS 26.4.1
CPU: (10) arm64 Apple M4
Memory: 178.25 MB / 16.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 22.15.0 - /Users/jeremyfunk/.local/state/fnm_multishells/6412_1781534594231/bin/node
npm: 10.9.2 - /Users/jeremyfunk/.local/state/fnm_multishells/6412_1781534594231/bin/npm
bun: 1.3.8 - /opt/homebrew/bin/bun
npmPackages:
@cloudflare/workers-types: ^4.20260603.1 => 4.20260615.1
wrangler: ^4.41.0 => 4.100.0
### Please provide a link to a minimal reproduction
https://github.com/JeremyFunk/workerd-datenow-mismatch
### Describe the Bug
The behaviour of Date.now() in the global scope varies between miniflare dev and a deployed worker.
### Miniflare dev behaviour
In Miniflare dev, the `Date.now()` function returns epoch time both in the global scope (so when getting the value on startup of the worker) and in request time. Interstingly, `performance.now()` is 0 on startup, and becomes epoch in requests.
Here is a condensed of the code I used to verify this ([see attached repo](https://github.com/JeremyFunk/workerd-datenow-mismatch)).
```ts
const globalDateNow = Date.now()
const globalPerformanceNow = performance.now()
const handler = async (): Promise => {
const requestDateNow = Date.now()
const requestPerformanceNow = performance.now()
const log = `
globalDateNow: ${globalDateNow}
globalPerformanceNow: ${globalPerformanceNow}
requestDateNow: ${requestDateNow}
requestPerformanceNow: ${requestPerformanceNow}
`
return new Response(log, { headers: { "content-type": "text/plain; charset=utf-8" }})
}
```
This code results in the following output:
```
globalDateNow: 1781534905975
globalPerformanceNow: 0
requestDateNow: 1781535318562
requestPerformanceNow: 1781535318563
```
Note that `performance.now()` and `Date.now()` **disagree** here on startup.
### Deployed worker behaviour
Executing the same code in production yields this output:
```
globalDateNow: 0
globalPerformanceNow: 0
requestDateNow: 1781535318562
requestPerformanceNow: 1781535318563
```
Note that `performance.now()` and `Date.now()` **do not disagree** here, they are identical in both cases.
### What the docs state
In the [Cloudflare workers performance API docs](https://developers.cloudflare.com/workers/runtime-apis/performance/) it states that, in production deployed workers, `performacne.now()` and `Date.now()` only advance or increment after I/O occurs. While in local development, timers increment regardless of whether I/O happens.
I guess that explains part of the issue, why `globalDateNow` and `requestDateNow` disagree between prod and dev. But the docs do imply that `performance.now()` and `Date.now()` work identical in this regard, which they don't.
## Impact
This disagree causes the span start time of [effect's](https://github.com/Effect-TS/effect-smol) tracer to be incorrect. Effect precomputes the time origin on startup. The exact beahviour of time origin can vary between runtimes, some runtimes do require an offset.
The bug is only triggered on effect's side when `nodejs-compat` is enabled with wrangler, which causes the code to go down a different branch on startup. In this branch they calculate the offset of time based on the time values present on startup, which are then corrected on request time. But since this value is precomputed, the incorrect time offset is reused.
The underlying bug is probably still with miniflare, as that has an undocumented mismatch in behaviour between production and dev enviornments. If `gloablDateNow` and `globalPerformanceNow` had identical behaviour, regardless of whether they were showing epoch or 0, the bug would not be triggered in Effect.
I created a separate issue for effect [here](https://github.com/Effect-TS/effect-smol/issues/2416).
---
Left out here for bravity, but relevant to the effect specific issue, since they use that function (included in linked repo):
The value of `process.hrtime.bigint()` tracks that of `Date.now()`, so it is 0 on startup in production, and epoch in miniflare dev. This is actually what causes the bug in effect, and is why nodejs-compat is required to trigger it (since process.hrtime.bigint() is not present in the globalThis.process.hrtime path in the runtime otherwise). But the `Date.now()` disagree happens regardless of whether `nodejs_compat` is enabled.
### Please provide any relevant error logs
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.