How to change timezone in chrome for testing
Send Emulation.setTimezoneOverride with an IANA id over the DevTools Protocol. Chrome 152 moved Intl.DateTimeFormat().resolvedOptions().timeZone from Europe/Kiev to America/New_York and the offset from -180 to 240, with no change to the machine clock. The timestamp the server had rendered stayed where it was.
Why check this
Run this whenever a bug report names a time that is wrong by a whole number of hours, and in regression on any screen that shows a date to a user in another country.
The defect this finds is a page that renders half its times in the viewer's zone and half in the server's. It is invisible from one zone, because both halves agree there. Setting the browser to a second zone separates them in one reload, and the step below shows the two lines drifting apart while sitting next to each other in the same paragraph.
Prerequisites
- Chrome 120 or later and Node 22.
npm i puppeteer-coredrives the Chrome already installed, so nothing is downloaded. Theopen()launcher is printed in full in How to check if a service worker is registered. - Emulation.setTimezoneOverride in the protocol reference. Its own note covers the empty string and nothing else used below.
- A page that prints the zone from both sides. Save it as
server.mjs, runnode server.mjs, and stop it by PID when finished. Port 8913 was free on this machine.
// server.mjs: one timestamp, rendered once by the server and once by the browser.
import { createServer } from 'node:http';
const PORT = 8913;
createServer((req, res) => {
const serverZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const iso = '2026-07-04T22:30:00Z';
if (req.url === '/zone') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ serverZone, rendered: new Date(iso).toLocaleString('en-US') }));
return;
}
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end(`<!doctype html><meta charset="utf-8"><title>zone</title>
<p id="server">server rendered: ${new Date(iso).toLocaleString('en-US')} (${serverZone})</p>
<p id="client"></p>
<script>
document.getElementById('client').textContent =
'browser rendered: ' + new Date('${iso}').toLocaleString('en-US') +
' (' + Intl.DateTimeFormat().resolvedOptions().timeZone + ')';
</script>`);
}).listen(PORT, '127.0.0.1', () => console.log('listening on ' + PORT));
- The figures are one capture, Chrome 152.0.7977.76 on Windows 11, on 2026-09-12. The machine zone was Europe/Kiev and the browser UI language was uk-UA, which is why the zone name inside
Date.toString()is in Ukrainian.
Steps
- Step 1.
Open the page and read what the browser thinks the zone is.
await s.goto('http://127.0.0.1:8913/'); await s.page.evaluate(() => ({ 'Intl…timeZone': Intl.DateTimeFormat().resolvedOptions().timeZone, 'getTimezoneOffset()': new Date().getTimezoneOffset(), 'Date.toString()': new Date('2026-07-04T22:30:00Z').toString(), 'navigator.language': navigator.language, }));--- 1 before any override { "Intl…timeZone": "Europe/Kiev", "getTimezoneOffset()": -180, "Date.toString()": "Sun Jul 05 2026 01:30:00 GMT+0300 (за східноєвропейським літнім часом)", "navigator.language": "uk-UA" }Four readings, all from the machine.
getTimezoneOffsetcounts minutes to add to local time to reach UTC, so a zone ahead of UTC gives a negative number. - Step 2.
Set the override and take the same four readings again.
await s.cdp.send('Emulation.setTimezoneOverride', { timezoneId: 'America/New_York' });--- 2 Emulation.setTimezoneOverride America/New_York: accepted --- 2 after the override { "Intl…timeZone": "America/New_York", "getTimezoneOffset()": 240, "Date.toString()": "Sat Jul 04 2026 18:30:00 GMT-0400 (за північноамериканським східним літнім часом)", "navigator.language": "uk-UA" }The zone, the offset and the rendered date all moved, with no reload and no change to the machine.
navigator.languagedid not move: this command changes the clock, not the locale. - Step 3.
Read the two paragraphs on the page, then reload and read them again.
await s.page.evaluate(() => ({ '#server': document.getElementById('server').textContent, '#client': document.getElementById('client').textContent, })); await s.page.reload({ waitUntil: 'networkidle2' });--- 3 the two lines on the page, no reload yet { "#server": "server rendered: 7/5/2026, 1:30:00 AM (Europe/Kiev)", "#client": "browser rendered: 7/5/2026, 1:30:00 AM (Europe/Kiev)" } --- 3 the two lines after a reload { "#server": "server rendered: 7/5/2026, 1:30:00 AM (Europe/Kiev)", "#client": "browser rendered: 7/4/2026, 6:30:00 PM (America/New_York)" }Before the reload both lines still read Europe/Kiev: the client line was written at load time and the override arrived after it. After the reload the browser line moves seven hours and the server line does not move at all.
- Step 4.
Ask the server directly, so the previous step is not read as a caching artefact.
await s.page.evaluate(() => fetch('/zone').then((r) => r.json()));--- 3 GET /zone { "serverZone": "Europe/Kiev", "rendered": "7/5/2026, 1:30:00 AM" }The request came from the overridden page and the response still carries the server's zone. Nothing about the override crosses the network.
- Step 5.
Send an id the tzdata does not have, once with no override in force and once with one.
const set = async (id) => { try { await s.cdp.send('Emulation.setTimezoneOverride', { timezoneId: id }); return 'accepted'; } catch (e) { return 'rejected: ' + e.message.replace(/^Protocol error \(.*?\): /, '').split('\n')[0]; } };--- 4 invalid and alias ids America/Atlantis, no override in force : rejected: Invalid timezone id -> Europe/Kiev America/New_York : accepted -> America/New_York America/Atlantis, override in force : accepted -> America/New_York EST : accepted -> America/PanamaThe same bad id is rejected on the first call and accepted on the third, leaving America/New_York in force.
ESTis accepted and resolves to America/Panama, a zone with no daylight saving. - Step 6.
Clear the override with an empty string and confirm the readings return.
await s.cdp.send('Emulation.setTimezoneOverride', { timezoneId: '' });--- 5 clear: accepted --- 5 after clearing { "Intl…timeZone": "Europe/Kiev", "getTimezoneOffset()": -180, "Date.toString()": "Sun Jul 05 2026 01:30:00 GMT+0300 (за східноєвропейським літнім часом)", "navigator.language": "uk-UA" }All four readings match step 1. A suite that leaves an override in place hands it to whatever runs next in the same tab.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Intl…timeZone reports the id you sent | The override is in force for this target | Reload before reading anything the page rendered at load time. |
| A rendered line does not move after a reload | That timestamp was formatted on the server | Send the instant and format it in the browser, or the viewer sees the server's zone. |
| getTimezoneOffset() is positive | The emulated zone is behind UTC | Expected for the Americas. The sign is the opposite of the GMT label. |
| The read-back id differs from the one you sent | A link or an alias was resolved | EST came back as America/Panama. Assert on the read-back, not on the string you sent. |
| navigator.language is unchanged | Only the clock moved | Change the locale separately if the format is also under test. |
Common mistakes
What to check next
- How to test timezone handling: what to assert once the browser is in a second zone.
- How to test daylight saving time: the transition dates that make the offset move within one zone.
- How to run jest tests with a specific timezone: the same change for a suite that has no browser in it.
- How to change locale in chrome: the setting this command deliberately leaves alone.
- How to check timezone stored in database: the other end of the timestamp that arrives at the page.
FAQ
How do I check the browser timezone?
Run Intl.DateTimeFormat().resolvedOptions().timeZone in the console. It returns an IANA id such as Europe/Kiev. new Date().getTimezoneOffset() gives the current offset in minutes, with the sign inverted against the GMT label.
Can I change the timezone in DevTools without a script?
Any DevTools Protocol client reaches the same command. Puppeteer wraps it as page.emulateTimezone('Asia/Tokyo'), which returned Asia/Tokyo here, and Playwright takes timezoneId when a context is created. All three end at Emulation.setTimezoneOverride.
Does the override survive a reload?
Yes. It is set on the target, not on the document. Step 3 reloads the page and the browser line comes back in the emulated zone. It ends when the empty string clears it or the target closes.
Why does the date still look wrong after I set the zone?
Check whether the value was rendered by the server. In the capture the server paragraph stayed at 1:30 AM Europe/Kiev through the override and the reload, because the string was built before the response left the server.
Is changing the machine clock equivalent?
No, and it is worse. The override affects one browser target for the length of the run. The system setting moves certificates, tokens and every other process on the machine, and it outlives the test.
Verified
Verified by Maks VernyChrome 152.0.7977.76Node 22.23.2puppeteer-core 25.10.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate9 minpublished updated Maks Verny