Clerk: redirect loop after authentication [2026 fix]
Clerk redirect loop occurs when `signInUrl` or `signUpUrl` points to protected routes. Fix: ensure auth URLs point to public pages.
Clerk: redirect loop after authentication [2026 fix]
TL;DR
Cause: Your signInUrl or signUpUrl configuration points to a route that's protected by <ProtectedRoute> or middleware, creating a loop where authenticated users get redirected back to auth.
Fix: Point signInUrl and signUpUrl to completely public, unprotected routes like /sign-in and /sign-up instead of /dashboard or authenticated pages.
---
Real Console Error Messages
``` 1. ERR_TOO_MANY_REDIRECTS: The page isn't redirecting properly
2. GET /dashboard 302 redirect → GET /sign-in 302 redirect → GET /dashboard
3. [Clerk] Redirect loop detected: signInUrl points to protected route
4. NextJS ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent to the client
5. Infinite redirect loop: auth middleware → sign-in page → auth check → redirect again ```
---
Broken Code vs. Exact Fix
Problem 1: signInUrl Points to Protected Route
BROKEN: ```javascript // app.tsx or clerk config <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY} signInUrl="/dashboard" signUpUrl="/dashboard/register" > <App /> </ClerkProvider> ```
FIXED: ```javascript // app.tsx - signInUrl MUST be public <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY} signInUrl="/sign-in" signUpUrl="/sign-up" afterSignInUrl="/dashboard" afterSignUpUrl="/dashboard" > <App /> </ClerkProvider> ```
Problem 2: Middleware Protecting Auth Routes
BROKEN: ```typescript // middleware.ts export const middleware = (req: NextRequest) => { const userId = req.nextauth.session?.user?.id; if (!userId) { return NextResponse.redirect(new URL('/sign-in', req.url)); } };
export const config = { matcher: ['/((?!_next|static).*)'], // Protects EVERYTHING including /sign-in }; ```
FIXED: ```typescript // middleware.ts - exclude auth routes export const middleware = (req: NextRequest) => { const userId = req.nextauth.session?.user?.id; if (!userId) { return NextResponse.redirect(new URL('/sign-in', req.url)); } };
export const config = { matcher: [ '/((?!_next|static|sign-in|sign-up|api/auth).*)' // ^^ Explicitly exclude public auth routes ], }; ```
Problem 3: ProtectedRoute Component in Auth Flow
BROKEN: ```jsx // pages/sign-in.jsx import ProtectedRoute from '@/components/ProtectedRoute';
export default function SignIn() { return ( <ProtectedRoute> <SignInForm /> </ProtectedRoute> ); } ```
FIXED: ```jsx // pages/sign-in.jsx - NO protection wrapper import { SignIn } from '@clerk/nextjs';
export default function SignInPage() { return <SignIn />; // Clerk handles auth state internally } ```
---
Key Configuration Rules
1. signInUrl + signUpUrl must point to pages that are never protected
2. afterSignInUrl + afterSignUpUrl point to where users go *after* auth (can be protected)
3. Middleware matcher must exclude /sign-in, /sign-up, and /api/auth/*
4. Never wrap Clerk components in your own ProtectedRoute wrapper
---
Still Broken? Check These Too
1. Catch-All Routes Protecting Auth Pages
If you haveapp/[...slug]/page.tsx or pages/[...catch].tsx with auth logic, it will catch /sign-in requests. Use explicit routes instead or add early returns for auth paths.2. Environment Variables Not Reloaded
After changingNEXT_PUBLIC_CLERK_SIGN_IN_URL, you must restart your dev server. Environment variables loaded at build time won't update on hot reload. Kill the process and run npm run dev again.3. Multiple Clerk Providers (Duplicate Config)
If you have Clerk configured in both_app.tsx AND a wrapper component, conflicting signInUrl values cause loops. Keep ClerkProvider at the root only—remove from component tree. Check related guide on [Clerk configuration conflicts](/?guide=clerk-multiple-providers).---
Version Clarification
This guide applies to:
I am uncertain whether older v3.x versions had different redirect behavior around afterSignInUrl—check their [legacy docs](https://clerk.com/docs/upgrade-guides) if on older versions.
---
Testing Your Fix
```bash
1. Clear browser cache (redirect loops cache)
Ctrl+Shift+Delete # or Cmd+Shift+Delete on Mac2. Test unauthenticated access
curl -I http://localhost:3000/dashboardShould redirect to /sign-in, NOT loop
3. Sign in, then verify redirect
Should land on /dashboard, NOT /sign-in again
```---
See Also
---
Found a different variation? Drop it in the comments—2am bugs love company.