How to Fix: Page Router – 404.js Refresh Infinity
Next.js Page Router 404.js Infinite Refresh: Why It Happens and How to Fix It
A custom 404.js page that keeps refreshing is usually not a rendering bug in isolation—it is a routing loop caused by code inside the not-found page, middleware, redirects, or data-loading logic that keeps sending the browser back into the same unmatched route flow.
In a Next.js Page Router application, pages/404.js must behave like a simple fallback UI. If that page performs navigation, depends on unstable client-side code, or gets intercepted by redirect logic, the browser can enter an infinite refresh cycle. This often looks like the page is constantly reloading even though the actual problem is repeated route resolution.
Understanding the Root Cause
With the Pages Router, Next.js serves pages/404.js whenever no route matches. That file should be static and safe. The infinite refresh problem usually appears when one of these conditions is true:
- The 404 page triggers navigation on mount, such as calling
router.push(),router.replace(), or changingwindow.location. - Middleware or next.config.js redirects rewrite an unknown route into another route that also resolves back to 404.
- A global auth guard in
_app.jsor middleware treats the 404 page like a protected route and redirects repeatedly. - Client-side effects depend on values that change every render, causing repeated navigation or state resets.
- Custom server or proxy behavior rewrites not-found requests incorrectly.
For example, this pattern is dangerous inside pages/404.js:
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export default function Custom404() {
const router = useRouter()
useEffect(() => {
router.replace('/')
}, [router])
return <h1>Not Found</h1>
}
If the destination route is also affected by redirect logic, or if the app immediately re-enters the unmatched route state, the result is an endless loop. The browser appears to refresh forever, but technically the app is continually navigating.
Another common source is middleware like this:
import { NextResponse } from 'next/server'
export function middleware(request) {
const { pathname } = request.nextUrl
if (!pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
If /login does not exist, or if the matcher catches every unknown route without exclusions, the app can bounce between redirect handling and the 404 route.
Step-by-Step Solution
The safest fix is to make pages/404.js a pure presentational page, then audit every redirect layer around it.
1. Keep pages/404.js static and side-effect free
Your custom 404 page should not fetch unstable data, run navigation on mount, or depend on redirect logic.
export default function Custom404() {
return (
<main>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
</main>
)
}
If you want a button back to safety, use a normal link instead of automatic navigation:
import Link from 'next/link'
export default function Custom404() {
return (
<main>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<Link href="/">Go back home</Link>
</main>
)
}
2. Remove router.push, router.replace, or window.location from 404.js
If your current file contains logic like this, remove it:
useEffect(() => {
router.replace('/')
}, [router])
A 404 page should display a not-found state, not force a redirect unless you are intentionally implementing a controlled recovery flow elsewhere.
3. Check _app.js for global redirects or auth guards
If you have route protection in pages/_app.js, ensure it excludes known public routes, especially /404.
import { useRouter } from 'next/router'
import { useEffect } from 'react'
const PUBLIC_ROUTES = ['/', '/login', '/404']
export default function MyApp({ Component, pageProps }) {
const router = useRouter()
useEffect(() => {
const isPublic = PUBLIC_ROUTES.includes(router.pathname)
if (!isPublic) {
const isAuthenticated = true
if (!isAuthenticated) {
router.replace('/login')
}
}
}, [router.pathname])
return <Component {...pageProps} />
}
If your auth check runs for every path and redirects blindly, the 404 page can never settle.
4. Review middleware.ts or middleware.js carefully
If you use Next.js middleware, narrow the matcher and exclude static files, APIs, and routes that should not redirect.
import { NextResponse } from 'next/server'
export function middleware(request) {
const { pathname } = request.nextUrl
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/api') ||
pathname === '/favicon.ico' ||
pathname === '/404' ||
pathname === '/login'
) {
return NextResponse.next()
}
const isAuthenticated = true
if (!isAuthenticated) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
The key idea is simple: do not let middleware create a loop that keeps recapturing the request after redirect.
5. Validate redirects in next.config.js
Look for broad redirects that may rewrite unknown pages into invalid destinations.
module.exports = {
async redirects() {
return [
{
source: '/old-path',
destination: '/new-path',
permanent: false,
},
]
},
}
Avoid catch-all patterns unless you fully understand how they interact with your route tree and fallback behavior.
6. Test with a minimal 404 page
When debugging, reduce the page to the smallest possible version:
export default function Custom404() {
return <h1>404</h1>
}
If the loop disappears, the issue is inside your original page code. If it remains, the issue is outside the page—usually middleware, app-wide effects, or custom server routing.
7. Restart the dev server and test in production mode
Some routing issues can look worse in development because of hot reload behavior. Always verify with a clean build:
npm run build
npm run start
If the bug only appears in development, inspect local effects, React Strict Mode behavior, and any code that runs on mount more than once.
8. Compare against the official Pages Router 404 behavior
If needed, verify expected behavior with the Next.js custom error pages documentation. A standard pages/404.js should render once and stay stable.
Common Edge Cases
- React Strict Mode double-invokes effects in development: This can make a bad redirect effect look like an infinite refresh even faster.
- Using router.asPath in a redirect effect: If the effect depends on a changing path value, it can retrigger continuously.
- 404 page importing browser-only code: Some third-party libraries touch
windowor mutate location state unexpectedly. - Middleware protecting all routes including assets: If CSS, JavaScript, or image requests are redirected, the app can behave unpredictably.
- Catch-all dynamic routes: A route like
pages/[...slug].jsmay intercept paths you expected to fall into404.js. - Base path or reverse proxy misconfiguration: Proxies can rewrite unknown paths repeatedly before Next.js resolves the request.
- Login page also missing or redirecting: If auth middleware redirects to a broken destination, the app can enter a persistent not-found loop.
FAQ
Why does my 404 page keep refreshing only in development?
The most common reason is a useEffect redirect combined with React Strict Mode, which intentionally re-runs effects during development. That exposes routing loops more aggressively than production.
Can middleware cause a 404 infinite refresh even if 404.js is correct?
Yes. A perfectly valid 404.js can still loop if middleware, next.config.js redirects, or auth guards keep rewriting the request. If a minimal 404 page still refreshes, inspect those layers first.
Should I redirect users automatically from 404.js?
Usually no. A custom not-found page should display a stable message and offer manual navigation with a link. Automatic redirects from 404 pages often hide real routing problems and can easily create loops.
The practical fix is to treat pages/404.js as a static endpoint, then remove any redirect logic that touches unmatched routes unless it is explicitly guarded. Once the 404 page becomes side-effect free and middleware exclusions are correct, the infinite refresh behavior stops.