Next.js Middleware Redirect Loop Broke Our Client Login cover image
Back to Blog
TechnologyPublished 1 July 2026ยท Updated 22 August 2026ยท 5 min read

Next.js Middleware Redirect Loop Broke Our Client Login

A production Next.js 14 dashboard got stuck in an infinite redirect loop between /login and /dashboard after a routine deploy. Here is the exact fix we shipped.

The Incident: Login Redirect Loop on a Production Client Dashboard

Last month we shipped a Next.js 14 dashboard for a Pune-based SaaS client. The auth flow lived in middleware.ts, guarding every route under /dashboard and bouncing unauthenticated users to /login. It worked in staging. It broke in production the moment we flipped the deploy.

Users reported being stuck between /login and /dashboard after signing in. The browser tab spun forever. Chrome DevTools showed a 307 redirect from /dashboard to /login, then another 307 from /login back to /dashboard, repeating until the tab crashed.

We reproduced it locally with yarn dev on port 3000. The cookie was set correctly after login. The middleware saw the cookie. But the redirect still fired. The loop was real and it was in our code.

What We Tried and What Failed

First attempt: switch from NextResponse.redirect to NextResponse.rewrite. The idea was to avoid the redirect mechanism entirely and serve /login content without a 307. It did not help. The browser still saw a redirect because the middleware was still running on /login itself, and the matcher was catching it.

Second attempt: set x-middleware-cache to no-cache on the response. We read the Vercel issue thread NextJs 14 middleware redirect issue and saw someone claim this fixed it. We added response.headers.set('x-middleware-cache', 'no-cache') to every redirect. No change. The edge CDN was still caching the 307.

Third attempt: call router.refresh() on the client after login. We wired it into the onSuccess callback of our TanStack mutation. It worked sometimes. On hard refresh it worked. On client-side navigation it failed. The RSC prefetch cache was carrying stale cookie state, and router.refresh() only cleared it inconsistently.

None of these addressed the root cause. They were band-aids on a misconfigured matcher.

The Working Approach: Explicit Cache Control and Matcher Fixes

The real problem had two parts. First, the browser was caching the 307 redirect from /dashboard to /login. Second, the middleware matcher included /login, so when a logged-in user hit /login, the middleware redirected them to /dashboard, which redirected back to /login. Infinite loop.

We fixed it with three changes in middleware.ts:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session_token')?.value;
  const { pathname } = request.nextUrl;

  if (token && (pathname === '/login' || pathname === '/verify-otp')) {
    const response = NextResponse.redirect(new URL('/dashboard', request.url), { status: 303 });
    response.headers.set('x-middleware-cache', 'no-cache');
    return response;
  }

  if (!token && pathname.startsWith('/dashboard')) {
    const response = NextResponse.redirect(new URL('/login', request.url), { status: 303 });
    response.headers.set('x-middleware-cache', 'no-cache');
    return response;
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!login|_next/static|_next/image|favicon.ico).*)'],
};

Key changes:

We also added export const dynamic = 'force-dynamic' to app/login/page.tsx to prevent edge prerendering of the login page. The Amplify discussion Next.js Middleware/Proxy redirect loop flagged that /login with no dynamic markers gets prerendered and cached with cookies stripped from the cache key.

Pitfalls We Would Warn an Intern About

Never rely on default 307 redirects in middleware for auth flows. Browsers cache them. Always use 303.

Always set x-middleware-cache: no-cache on redirect responses. It is not a real CDN header, but it tells the Next.js client router not to cache the redirect in its internal cache. The 72Technologies blog calls this out as the most common oversight.

Ensure the middleware matcher excludes public routes like /login. If the matcher catches /login, and the middleware redirects logged-in users away from /login, you get a loop. The Stack Overflow thread NextJS Middleware causing too many redirects shows the same mistake with NextAuth.

Be cautious of stale RSC prefetch caches. When a user logs in via a Server Action, the client-side router may still hold the old cookie state in its prefetch cache. Calling router.refresh() on page mount of the login page, before the login action, is more reliable than calling it after. The Vercel issue thread confirms this.

What We Would Do Differently Next Time

We would move fine-grained session validation out of middleware and into the layout or page using cookies() and server-side session lookups. The 72Technologies blog describes this pattern: middleware only handles the obvious case of no cookie at all. The layout handles token verification, expiry checks, and user status.

We would also add automated tests for redirect behavior. A simple Playwright test that logs in and asserts no redirect loop would have caught this before deploy.

And we would use router.refresh() proactively on page mount for auth-related pages, not reactively after login. The Vercel issue thread shows this is the more reliable pattern in Next.js 15.2+.

The fix shipped. Users can log in again. The loop is gone. But we kept the checklist above pinned to our team Slack. Next time, we will not repeat this mistake.

Sources

Enjoyed this article?

Back to Blog