A React hydration error only in production usually means the production server renders something your machine does not: another timezone or locale, cached HTML, different data, or HTML that a CDN rewrites on the way. Production builds also say less, with minified codes, no diff and, in React 19, no report at all for attribute mismatches.
Why you get a React hydration error only in production
A random React hydration error 418 that shows up only on prod (on Vercel or any other host) usually has one of these causes:
| Cause | Why production differs | Fix guide |
|---|---|---|
| The server's timezone | Servers usually run in UTC; your laptop runs in your timezone | Timezone |
| The server's locale | Numbers and dates formatted with the server's default locale | Locale |
| Cached or prerendered HTML | A statically generated page was rendered at build time; "today" or "3 minutes ago" is days old when it hydrates | Time |
| HTML rewritten by a CDN | Minification or email obfuscation changes the HTML after it leaves your server | CDN |
| Scripts that only run in production | Analytics, tag managers or A/B testing tools edit the DOM before React hydrates | Third-party scripts |
| Different data | Feature flags, personalization or a second fetch on the client return other values | Data |
| Your users' browsers | Extensions, translation tools and in-app browsers that you do not use | Extensions |
Some of these also happen in development but go unnoticed there. React 19 development builds warn about an attribute mismatch; production builds keep the server's attribute silently and never log it.
What production builds show you
React's production build replaces messages with codes:
Minified React error #418; visit https://react.dev/errors/418
Minified React error #423; visit https://react.dev/errors/423
Minified React error #425; visit https://react.dev/errors/425- #418: hydration failed. In React 19 it means "Hydration failed because the server rendered HTML didn't match the client", and React renders the tree again on the client.
- #423 and #425: React 18's messages for a root that switched to client rendering and for text that did not match.
None of them names the element. Minified React error codes lists the others you may see alongside them.
How to debug a Next.js hydration error that only shows up in production
-
Build and run production locally. Most of these errors reproduce with a production build on your machine:
npm run build npm run start -
Use the server's timezone and locale. Start the server the way your host does. On Linux and macOS:
TZ=UTC LANG=en_US.UTF-8 npm run startIf the error appears now, the cause is timezone or locale formatting.
-
Turn on browser source maps. They map minified code back to your files:
next.config.ts import type { NextConfig } from "next"; const nextConfig: NextConfig = { productionBrowserSourceMaps: true, }; export default nextConfig; -
Compare what the origin sends with what the browser gets. If they differ, something between them rewrites the HTML:
curl -s https://origin.example.com/pricing > origin.html curl -s https://www.example.com/pricing > edge.html diff origin.html edge.html -
Check cached pages. If only prerendered or cached pages fail, look for the clock in render: the HTML was made at build time.
-
Test both builds with hydration-proof (below). Findings that appear only in the production build are marked as such.
Test production builds before you deploy
hydration-proof test tests the production build by default. --mode both starts a production build and the development server and tests every route in each, so you get React's full development messages and the production behavior in one run:
npx hydration-proof test --mode bothIt compares every attribute with the props React renders on the client, so it also finds the attribute mismatches React 19 never reports in production. To test a deployed preview instead, point it at the URL:
npx hydration-proof test --url https://preview.example.com --sitemapProduction servers rarely share your timezone. Add scenarios for the environments your users and your servers have:
import { defineConfig } from "hydration-proof";
export default defineConfig({
scenarios: [
{ name: "default" },
{ name: "utc", timezoneId: "UTC", locale: "en-US" },
{ name: "berlin", timezoneId: "Europe/Berlin", locale: "de-DE" },
],
});See scenarios and the environment matrix for more combinations, and CI to run this on every pull request.