
Server and browser calculate dates in different timezones, breaking your Next.js renders — here's how to fix it.
If you’ve ever seen a hydration mismatch error involving dates, or watched your “today” highlight flicker to the wrong day on page load, you’ve hit one of Next.js’s more annoying gotchas. It’s not a bug exactly — it’s just how JavaScript dates work. Which doesn’t make it less frustrating.
Here’s the short version: new Date() gives you different results on the server than it does in the browser.
Your Next.js server probably runs in UTC. Your browser runs in whatever timezone your OS is set to. When you do something like this:
import { startOfDay } from 'date-fns'
const today = startOfDay(new Date())The server might calculate today as 2026-01-17T00:00:00.000Z. But if you’re in Warsaw, your browser calculates it as 2026-01-16T23:00:00.000Z — because midnight in Warsaw is 11pm the previous day in UTC.
React sees the server-rendered HTML with one date, then the client tries to hydrate with a different date. Mismatch. Error. Console full of red.
This isn’t just a console warning you can ignore. It causes real problems:
And if you’re using Suspense or streaming (which, let’s be honest, the App Router pushes you toward), the problem gets worse because you can’t easily control when hydration happens.
One solution is to stop calculating dates dynamically during render. Consider this example:
export function TodayWidget() {
return (
// ...
<span>{new Date().toLocaleDateString()}</span>
// ...
)
}This will cause a hydration error. We could convert it to a client component and render in the user’s browser:
'use client'
export function TodayWidget() {
const [date, setDate] = useState<string>('')
useEffect(() => {
setDate(new Date().toLocaleDateString())
}, [])
return (
// ...
<span>{date || 'Loading...'}</span>
// ...
)
}This avoids the mismatch because the date only ever calculates on the client. But we’ve just turned a static string into a loading state, and created a client component to render a simple date. That’s not why we use Next.js — we want to leverage SSR.
In practice, we rarely render dates out of nowhere. Usually we fetch data (say, a blog post) and want to display its publish date. We can do this elegantly using JavaScript’s built-in Intl API:
type Story = {
title: string
body: string
publishedAt: string
}
export default async function BlogPostPage(props: {
params: Promise<{ slug: string }>
}) {
const { slug } = await props.params
const story: Story = await getStory(slug)
const publishedAtFormatted = new Intl.DateTimeFormat('en-US', {
timeZone: 'UTC',
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(new Date(story.publishedAt))
return (
<article>
<h1>{story.title}</h1>
<time dateTime={story.publishedAt}>{publishedAtFormatted}</time>
{story.body}
</article>
)
}The key here is timeZone: 'UTC' — it forces the formatter to use the same timezone regardless of where the code runs.
A few rules to keep your server-rendered dates consistent:
timeZone: 'UTC' in your formatting options.toLocaleDateString() without parameters will use different locales on different machines. Pass a consistent locale instead: toLocaleDateString('en-US').useEffect.If you prefer a minimal custom approach without Intl, you can extract the date portion from an ISO string:
const formatServerDate = (date: Date) => {
return date.toISOString().split('T')[0] // Returns YYYY-MM-DD
}This works because toISOString() always returns UTC — no timezone variance.
Once you’ve solved the hydration problem, you’ve tasted what proper regionalization in Next.js looks like: locale-aware date formatting, currency display, measurement systems. But that’s a bigger topic for another post, where I’ll show you how to set up a consistent locale context throughout your app.
For now, the takeaway is simple: if you’re rendering dates in a Next.js app with SSR, format them to plain strings where the output can’t differ between server and client. Your future self will thank you.