DEV Community

Cover image for Fix Next.js Middleware Redirect Loop: Why Your Login Page Keeps Redirecting
Amrishkhan Sheik Abdullah
Amrishkhan Sheik Abdullah

Posted on

Fix Next.js Middleware Redirect Loop: Why Your Login Page Keeps Redirecting

This is the third article in Real Coding Problems, Simple Fixes.

This one is about a problem that can waste hours because the app looks like it is doing the right thing:

Protect private pages and redirect logged-out users to /login.

But then the browser shows:

ERR_TOO_MANY_REDIRECTS
Enter fullscreen mode Exit fullscreen mode

or your Next.js app keeps sending users back to the login page again and again.

That is usually a Next.js middleware redirect loop.

The cause is often simple:

Your middleware is protecting the login page too.

Let's fix it with clear code.

What We Are Fixing

If you are dealing with this bug, it usually looks like one of these problems:

  • Next.js middleware redirect loop
  • Next.js login page keeps redirecting
  • ERR_TOO_MANY_REDIRECTS Next.js
  • NextAuth middleware redirect loop
  • Next.js middleware matcher exclude login page
  • Next.js protected routes middleware

We will use a simple auth-token example first, then talk about NextAuth/Auth.js after.

The Real Problem

Imagine your app has these routes:

/login
/dashboard
/settings
/api/auth/login
Enter fullscreen mode Exit fullscreen mode

You want this behavior:

Logged out user opens /dashboard
Redirect to /login

Logged out user opens /login
Show the login page

Logged in user opens /dashboard
Show the dashboard
Enter fullscreen mode Exit fullscreen mode

That sounds straightforward.

So you write middleware to check whether the user has an auth token.

The Broken Middleware

Here is a common broken version:

import { NextResponse } from "next/server";

export function middleware(request) {
  const token = request.cookies.get("auth_token")?.value;

  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}
Enter fullscreen mode Exit fullscreen mode

This looks reasonable at first.

If there is no token, send the user to /login.

But there is a hidden problem.

This middleware runs for more routes than you think.

So the request flow becomes:

User opens /dashboard
No token found
Redirect to /login

Browser opens /login
Middleware runs again
Still no token found
Redirect to /login again

Browser opens /login again
Middleware runs again
Still no token found
Redirect to /login again
Enter fullscreen mode Exit fullscreen mode

That is the loop.

The login page can never load because the middleware keeps redirecting it to itself.

The Plain-English Explanation

Middleware runs before the page is rendered.

That makes it useful for auth checks, redirects, headers, rewrites, and request decisions.

But it also means you need to be specific.

If your middleware says:

Every logged-out request must go to /login

then /login itself also becomes a logged-out request.

So the app gets stuck.

The fix is not complicated:

Do not run auth protection on public routes.

Your public routes usually include:

  • /login
  • /register
  • /forgot-password
  • /api/auth
  • static files
  • images
  • favicon
  • auth callback routes

Fix 1: Add Public Route Checks

The first fix is to define which routes are public.

import { NextResponse } from "next/server";

const publicRoutes = ["/login", "/register", "/forgot-password"];

function isPublicRoute(pathname) {
  return publicRoutes.some((route) => pathname.startsWith(route));
}

export function middleware(request) {
  const { pathname } = request.nextUrl;
  const token = request.cookies.get("auth_token")?.value;

  if (isPublicRoute(pathname)) {
    return NextResponse.next();
  }

  if (!token) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("from", pathname);

    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}
Enter fullscreen mode Exit fullscreen mode

Now the request flow is different:

User opens /dashboard
No token found
Redirect to /login?from=/dashboard

Browser opens /login
/login is public
Show login page
Enter fullscreen mode Exit fullscreen mode

That small public route check breaks the loop.

Fix 2: Use matcher to Protect Only Private Routes

Another good fix is to tell Next.js exactly where the middleware should run.

If only /dashboard and /settings need protection, the middleware should not run everywhere.

import { NextResponse } from "next/server";

export function middleware(request) {
  const token = request.cookies.get("auth_token")?.value;

  if (!token) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("from", request.nextUrl.pathname);

    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*"],
};
Enter fullscreen mode Exit fullscreen mode

This is often cleaner.

With this setup:

/dashboard      -> middleware runs
/dashboard/team -> middleware runs
/settings       -> middleware runs
/login          -> middleware does not run
/api/auth/login -> middleware does not run
Enter fullscreen mode Exit fullscreen mode

For many apps, this is the simplest and safest solution.

Which Fix Should You Use?

Use matcher when you have a clear list of private route groups.

Example:

/dashboard
/account
/admin
/settings
Enter fullscreen mode Exit fullscreen mode

Use public route checks when your app has more complex rules.

Example:

Some pages are public
Some pages are private
Some pages depend on role
Some pages depend on onboarding status
Enter fullscreen mode Exit fullscreen mode

You can also combine both:

  • use matcher to avoid static files and unrelated routes
  • use route checks inside middleware for auth-specific decisions

Fix 3: Avoid Redirecting Logged-In Users Back to Login

There is another common issue.

After login, the user visits /login again and still sees the login page.

Usually, logged-in users should be redirected away from auth pages.

You can handle that like this:

import { NextResponse } from "next/server";

const authRoutes = ["/login", "/register"];
const privateRoutes = ["/dashboard", "/settings"];

function matchesRoute(pathname, routes) {
  return routes.some((route) => pathname.startsWith(route));
}

export function middleware(request) {
  const { pathname } = request.nextUrl;
  const token = request.cookies.get("auth_token")?.value;

  const isAuthRoute = matchesRoute(pathname, authRoutes);
  const isPrivateRoute = matchesRoute(pathname, privateRoutes);

  if (isAuthRoute && token) {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }

  if (isPrivateRoute && !token) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("from", pathname);

    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}
Enter fullscreen mode Exit fullscreen mode

Now the rules are clear:

Logged-out user on /dashboard -> send to /login
Logged-out user on /login -> allow
Logged-in user on /login -> send to /dashboard
Logged-in user on /dashboard -> allow
Enter fullscreen mode Exit fullscreen mode

That is the mental model you want.

Fix 4: Exclude API Routes and Static Files

If your middleware runs too broadly, it can also touch routes it should ignore.

For example:

/_next/static
/_next/image
/favicon.ico
/api/auth/login
/api/auth/callback
Enter fullscreen mode Exit fullscreen mode

You usually do not want your page-auth middleware interfering with those.

Here is a common matcher pattern:

export const config = {
  matcher: [
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};
Enter fullscreen mode Exit fullscreen mode

This tells middleware to run on most pages, but skip API routes and common Next.js asset paths.

Be careful with this pattern though.

If your login page is still inside that matched group, you still need a public route check inside middleware.

That is why I prefer being explicit for beginner-friendly auth:

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*", "/account/:path*"],
};
Enter fullscreen mode Exit fullscreen mode

It is easier to reason about.

NextAuth/Auth.js Redirect Loop Note

If you use NextAuth or Auth.js, redirect loops usually come from one of these problems:

  1. Your custom sign-in page is also protected by middleware.
  2. Your middleware does not know about your custom signIn page.
  3. Your callback or auth API route is being matched accidentally.
  4. The user is redirected after login to a route that immediately fails another middleware rule.

For a custom sign-in page, make sure your auth config and middleware agree about the sign-in route.

Example idea:

export const authOptions = {
  pages: {
    signIn: "/login",
  },
};
Enter fullscreen mode Exit fullscreen mode

And make sure /login is not treated like a protected page.

If your app uses a matcher like this:

export const config = {
  matcher: ["/dashboard/:path*"],
};
Enter fullscreen mode Exit fullscreen mode

then /login will not be protected by that middleware.

That alone fixes many redirect loops.

Common Mistakes

Mistake 1: Matching Every Route

This is risky:

export const config = {
  matcher: ["/:path*"],
};
Enter fullscreen mode Exit fullscreen mode

It can work, but only if your internal route logic is careful.

If you are new to middleware, start by matching only private sections.

Mistake 2: Redirecting to the Same URL

Before redirecting, think:

Am I already on the page I am redirecting to?
Enter fullscreen mode Exit fullscreen mode

If the user is already on /login, do not redirect them to /login.

Mistake 3: Protecting Auth Callback Routes

Auth libraries often need callback routes to finish login.

If middleware blocks those routes, login may succeed with the provider but fail when the app receives the callback.

Watch routes like:

/api/auth/callback
/api/auth/session
/auth/callback
Enter fullscreen mode Exit fullscreen mode

The exact paths depend on your auth library.

Mistake 4: Forgetting Role-Based Redirect Loops

Redirect loops are not only about login.

They can also happen with roles.

Example:

User opens /admin
Middleware says user is not admin
Redirect to /dashboard

Middleware checks /dashboard
Onboarding is incomplete
Redirect to /onboarding

Middleware checks /onboarding
Some rule sends user back to /dashboard
Enter fullscreen mode Exit fullscreen mode

The fix is the same:

Make every redirect rule clear, and make sure the destination route is allowed for that user state.

A Simple Debugging Checklist

When your Next.js login page keeps redirecting, check this:

  1. Does middleware run on /login?
  2. Does middleware run on /api/auth?
  3. Does your matcher include too many routes?
  4. Are you redirecting to the same route you are already on?
  5. Does your custom sign-in page match your auth config?
  6. Are callback routes excluded?
  7. After login, are you redirecting to a route the user is actually allowed to access?

If you are stuck, add temporary logging:

export function middleware(request) {
  console.log("middleware path:", request.nextUrl.pathname);

  return NextResponse.next();
}
Enter fullscreen mode Exit fullscreen mode

Then reload the page and watch which routes middleware is touching.

The path list usually reveals the problem quickly.

The Mental Model

Middleware redirect bugs become easier when you think in flows.

Do not only ask:

Is the user logged in?
Enter fullscreen mode Exit fullscreen mode

Also ask:

Which route is the user trying to open?
Is that route public or private?
If I redirect, will the destination route be allowed?
Will middleware run again on the destination route?
Enter fullscreen mode Exit fullscreen mode

That last question is the important one.

Because every redirect creates another request.

And middleware can run again on that new request.

Final Takeaway

A Next.js middleware redirect loop usually happens because the middleware is too broad.

Most commonly:

/dashboard redirects to /login
/login also runs middleware
/login redirects to /login again
Enter fullscreen mode Exit fullscreen mode

The practical fix:

Protect only private routes
+ Keep login/register pages public
+ Exclude auth API and callback routes
+ Use matcher carefully
+ Never redirect a route to itself
Enter fullscreen mode Exit fullscreen mode

Once your routes are separated into public, auth-only, and private groups, the loop becomes much easier to avoid.

Publishing SEO Notes

SEO title:
Fix Next.js Middleware Redirect Loop: Protect Routes Without Breaking Login

Meta description:
Learn why a Next.js middleware redirect loop happens, why login pages get accidentally protected, and how to fix protected routes with matcher and public route checks.

Suggested slug:
fix-nextjs-middleware-redirect-loop-protected-routes

Primary keyword:
Next.js middleware redirect loop

Secondary keywords:
Next.js auth redirect loop
Next.js login page keeps redirecting
ERR_TOO_MANY_REDIRECTS Next.js
NextAuth middleware redirect loop
Next.js middleware matcher exclude login page
Next.js protected routes middleware
Enter fullscreen mode Exit fullscreen mode

These notes are for publishing. You can keep them out of the visible article body if your CMS already has dedicated SEO fields.

Suggested Internal Links

  • Previous article: Fix Next.js Hydration Error with localStorage
  • Related topic idea: Fix Axios Refresh Token Infinite Loop in React and Node.js
  • Portfolio CTA: I write practical engineering guides and build full-stack products. See more at amrishkhan.dev.

References

Top comments (0)