Clerk: redirect loop after authentication [2026 fix]
Redirect URI mismatch between Clerk dashboard and app code causes infinite auth loops. Fix: sync your redirect URIs and remove duplicate middleware.
Clerk: Redirect Loop After Authentication
TL;DR
Cause: Your Clerk redirect URI in the dashboard doesn't match your app's callback route, or you have duplicate <ClerkProvider> wrappers causing middleware to run twice.
Fix: Verify NEXT_PUBLIC_CLERK_SIGN_IN_URL matches your actual callback endpoint, and ensure you're not wrapping your app in multiple <ClerkProvider> components.
---
Real Console Error Messages
When you hit this bug at 2am, watch for these exact messages:
``` Error: Clerk: redirect loop detected. Infinite redirects to /auth/callback ```
``` WARN [ClerkProvider] Detected multiple ClerkProvider instances. This causes duplicate middleware execution and redirect loops. ```
``` ERROR: Invalid redirect_uri. Expected "http://localhost:3000/auth/callback" but got "http://localhost:3000/sign-in" ```
``` TypeError: Cannot read property 'redirectUrl' of undefined at ClerkMiddleware.ts:45 ```
``` REDIRECT LOOP: 301 → /auth/callback → /sign-in → /auth/callback (repeated 25 times) ```
---
Broken Code vs. Fixed Code
Problem 1: Mismatched Redirect URIs
BROKEN: ```javascript // pages/api/auth/callback.ts export default async function handler(req, res) { // Clerk dashboard has: http://localhost:3000/auth/signin // But your code expects: /auth/callback res.redirect('/dashboard'); }
// .env.local NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard // Missing the actual callback route configuration ```
FIXED: ```javascript // pages/api/auth/callback.ts - Keep this endpoint export default async function handler(req, res) { res.redirect('/dashboard'); }
// .env.local - Add these matching variables NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
// Then in Clerk Dashboard > API Keys > Redirect URIs: // Add: http://localhost:3000/auth/callback (exactly) // Add: http://yourdomain.com/auth/callback (for production) ```
Problem 2: Duplicate ClerkProvider Wrappers
BROKEN: ```typescript // pages/_app.tsx import { ClerkProvider } from '@clerk/nextjs';
export default function App({ Component, pageProps }) { return ( <ClerkProvider> <Component {...pageProps} /> </ClerkProvider> ); }
// pages/index.tsx - Also wrapping here import { ClerkProvider } from '@clerk/clerk-react';
export default function Home() { return ( <ClerkProvider> <div>Content</div> </ClerkProvider> ); } ```
FIXED: ```typescript // pages/_app.tsx - Single provider at root import { ClerkProvider } from '@clerk/nextjs';
export default function App({ Component, pageProps }) { return ( <ClerkProvider> <Component {...pageProps} /> </ClerkProvider> ); }
// pages/index.tsx - Remove the wrapper export default function Home() { return ( <div>Content</div> ); } ```
Problem 3: Middleware Configuration
BROKEN: ```typescript // middleware.ts import { authMiddleware } from '@clerk/nextjs';
export const config = { matcher: ['/(.*?)'], // Too broad, catches everything };
export default authMiddleware({ publicRoutes: ['/'], // Missing sign-in and sign-up routes }); ```
FIXED: ```typescript // middleware.ts import { authMiddleware } from '@clerk/nextjs';
export const config = { matcher: [ '/((?!_next|_clerk|.*\\..*|api/|public).*)', '/api/protected(.*)', ], };
export default authMiddleware({ publicRoutes: ['/', '/sign-in', '/sign-up', '/api/public(.*)'], ignoredRoutes: ['/api/webhook(.*)'], }); ```
---
Version-Specific Notes
Clerk SDK v4.x vs v5.x behavior: We're uncertain whether the redirect URI validation changed between these versions. Check your package.json and cross-reference the [official Clerk changelog](https://clerk.com/changelog). If you're on v4.x and seeing different error messages, please comment below.
---
Still Broken? Check These Too
1. Environment variable typo – Copy-paste your NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY directly from the Clerk dashboard. A single character difference breaks everything. Restart your dev server after updating .env.local.
2. CORS/Origin mismatch – If deployed, verify your production domain is registered in Clerk dashboard under "Allowed Origins." Check [Clerk deployment guide](/?guide=clerk-deployment).
3. Stale browser cache – Hard refresh (Ctrl+Shift+R or Cmd+Shift+R), clear cookies for localhost, and test in an incognito window. Also check [debugging authentication flows](/?guide=auth-debugging).
---
Official Resources
---
Found a different variation? Drop it in the comments – If you hit this error with a unique setup (custom domain, monorepo, serverless, etc.), share your solution to help future 2am debugging sessions.