# Fix a timezone hydration mismatch

> A timezone hydration mismatch: the server formats dates in UTC, the browser in the visitor's zone. Pass one explicit timeZone to both, or format after mount.

Source: https://hydration.jscrate.dev/docs/causes/timezone
Last updated: 2026-09-18

A timezone hydration mismatch happens when a component formats a date in the
runtime's time zone. The server usually runs in UTC and the browser in the
visitor's zone, so "5:00 AM" on the server becomes "10:00 AM" in Karachi. Pass
the same explicit `timeZone` to the formatter on both sides, or format the date
after mount.

## Symptoms

React reports a text difference, and only for visitors whose zone gives a
different result than the server's:

```text
Hydration failed because the server rendered text didn't match the client.
Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "Signed in at 5:00 AM" Client: "Signed in at 10:00 AM"
Minified React error #418; visit https://react.dev/errors/418
```

hydration-proof reports it as [HP1001](https://hydration.jscrate.dev/docs/issues/hp1001) with the cause
**Timezone difference**, and names both zones in the reason:

```text
HP1001 Text differs between server and client  (timezone difference, 95%)
  #last-login  in LastLogin
  server: "Signed in at 5:00 AM"
  client: "Signed in at 10:00 AM"
  app/dashboard/LastLogin.tsx:14:10
  → Pass an explicit timeZone to the formatter (the same on both sides), or format the date after mount.
```

Times differ for almost every visitor. Dates differ only near midnight, which
makes the bug look random: a "Posted on May 3" line breaks for some visitors in
the evening and for nobody in the morning.

## Why a timezone hydration mismatch happens

A `Date` holds one moment. Turning it into text or into parts (hours, day,
month) needs a time zone, and every JavaScript formatter uses the runtime's
zone unless you pass one.
Mixing local dates in the browser timezone with UTC dates on the server is the classic case.

These read the runtime's zone:

- `toLocaleDateString()`, `toLocaleTimeString()`, and `toLocaleString()` on a date
- `new Intl.DateTimeFormat()` without a `timeZone` option
- `getHours()`, `getMinutes()`, `getDate()`, `getDay()`, `getMonth()`,
  `getFullYear()` and `getTimezoneOffset()`
- `toDateString()`, `toTimeString()` and `toString()` on a date
- `Intl.DateTimeFormat().resolvedOptions().timeZone`, which returns the zone itself

The date does not need to be "now". `new Date(post.publishedAt)` is the same
moment on both sides, and still prints a different hour in Karachi than in UTC.
When the value really is the current time, fix that first: see
[date and time hydration errors](https://hydration.jscrate.dev/docs/causes/time).

## How to fix it

### Use one time zone on both sides

Decide the zone on the server and pass it down. The visitor's own zone works if
the server can know it, for example from a profile setting or a cookie the
browser set on an earlier visit:

```tsx title="app/dashboard/page.tsx"
import { cookies } from "next/headers";
import { LastLogin } from "./last-login";

export default async function Page() {
  const timeZone = (await cookies()).get("tz")?.value ?? "UTC";
  const user = await getUser();
  return <LastLogin at={user.lastLoginAt} timeZone={timeZone} />;
}
```

```tsx title="app/dashboard/last-login.tsx"
"use client";

export function LastLogin({ at, timeZone }: { at: number; timeZone: string }) {
  const time = new Date(at).toLocaleTimeString("en-US", {
    timeZone,
    hour: "numeric",
    minute: "2-digit",
  });
  return <p id="last-login">Signed in at {time}</p>;
}
```

Set the cookie from the browser once, after hydration, so the next request
renders in the right zone:

```tsx title="app/timezone-cookie.tsx"
"use client";

import { useEffect } from "react";

export function TimeZoneCookie() {
  useEffect(() => {
    const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
    document.cookie = `tz=${zone}; path=/; max-age=31536000; samesite=lax`;
  }, []);
  return null;
}
```

The first visit renders in UTC. Every visit after it matches the visitor.

### Intl.DateTimeFormat timeZone: hydration-safe formatting

When one zone is right for everyone (an event schedule, a log viewer, a
changelog), pin it in the formatter. Replace local getters with their UTC
versions:

```tsx title="schedule.tsx"
// Before: the runtime's zone, different on each side
const localFormat = new Intl.DateTimeFormat("en-US", {
  dateStyle: "medium",
  timeStyle: "short",
});

// After: the same zone everywhere
const format = new Intl.DateTimeFormat("en-US", {
  timeZone: "UTC",
  dateStyle: "medium",
  timeStyle: "short",
});

export function Posted({ at }: { at: number }) {
  const date = new Date(at);
  // getUTCDate() instead of getDate(), getUTCMonth() instead of getMonth()
  return (
    <p>
      {format.format(date)} ({date.getUTCDate()}/{date.getUTCMonth() + 1})
    </p>
  );
}
```

A locale-sensitive formatter also needs an explicit locale, or the two sides
still disagree about the format. See [locale formatting](https://hydration.jscrate.dev/docs/causes/locale).

### Format in the visitor's zone after mount

If the server cannot know the zone and the time must be local, render a neutral
value first and replace it in an effect. Effects never run on the server, so
the first client render still matches:

```tsx title="local-time.tsx"
"use client";

import { useEffect, useState } from "react";

export function LocalTime({ at }: { at: number }) {
  const [text, setText] = useState(() =>
    new Date(at).toISOString().slice(0, 10)
  );

  useEffect(() => {
    setText(
      new Date(at).toLocaleString("en-US", {
        dateStyle: "medium",
        timeStyle: "short",
      })
    );
  }, [at]);

  return <time dateTime={new Date(at).toISOString()}>{text}</time>;
}
```

The [two-pass rendering pattern](https://hydration.jscrate.dev/docs/guides/useeffect-two-pass-rendering)
explains the trade-off: the placeholder shows until the page hydrates.

### i18n libraries

next-intl and similar libraries format with a configured zone. Set `timeZone`
in the request config so the server and the client provider use the same one.
The [locale guide](https://hydration.jscrate.dev/docs/causes/locale#fix-an-i18n-hydration-error-in-next-intl)
shows the setup.

## Catch it with ESLint

[`no-timezone-without-explicit-timezone`](https://hydration.jscrate.dev/docs/rules/no-timezone-without-explicit-timezone)
reports every formatter and getter above in render code. It suggests adding
`timeZone: 'UTC'` (or the zone in its `defaultTimeZone` option) and swapping
`getHours()` for `getUTCHours()`. Server Components are skipped, because they
only render on the server.

```bash
npm install -D eslint-plugin-hydration-proof
```

## Catch it in CI

The default scenario uses the machine's own timezone, and a server started on
the same machine renders with it, so the two sides agree. Test other zones with
the environment matrix:

```ts title="hydration-proof.config.ts"
import { defineConfig } from "hydration-proof";

export default defineConfig({
  matrix: {
    timezoneId: ["UTC", "Asia/Karachi", "America/Los_Angeles"],
  },
});
```

Each finding is reported as HP1001, and the report names the values that
separate failing pages from passing ones ("Only found with timezone
Asia/Karachi"). With `--probe`, hydration-proof reloads the page with only the
browser's timezone switched to the server's. If the finding disappears, the
timezone is the proven cause. See the [environment matrix](https://hydration.jscrate.dev/docs/environment-matrix)
and [probes](https://hydration.jscrate.dev/docs/probes).

```bash
npx hydration-proof test --probe
```

## Related

- [Date and time hydration errors](https://hydration.jscrate.dev/docs/causes/time)
- [Locale-dependent formatting](https://hydration.jscrate.dev/docs/causes/locale)
- [HP1001: text differs between server and client](https://hydration.jscrate.dev/docs/issues/hp1001)
- [The no-timezone-without-explicit-timezone rule](https://hydration.jscrate.dev/docs/rules/no-timezone-without-explicit-timezone)
- [All causes of hydration errors](https://hydration.jscrate.dev/docs/causes)
