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).
const globalDateNow = Date.now()
const globalPerformanceNow = performance.now()
const handler = async (): Promise<Response> => {
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 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 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.
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
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).
This code results in the following output:
Note that
performance.now()andDate.now()disagree here on startup.Deployed worker behaviour
Executing the same code in production yields this output:
Note that
performance.now()andDate.now()do not disagree here, they are identical in both cases.What the docs state
In the Cloudflare workers performance API docs it states that, in production deployed workers,
performacne.now()andDate.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
globalDateNowandrequestDateNowdisagree between prod and dev. But the docs do imply thatperformance.now()andDate.now()work identical in this regard, which they don't.Impact
This disagree causes the span start time of effect's 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-compatis 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
gloablDateNowandglobalPerformanceNowhad 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.
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 ofDate.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 theDate.now()disagree happens regardless of whethernodejs_compatis enabled.Please provide any relevant error logs
No response