Clerk: redirect loop after authentication [2026 fix]
Redirect loop after Clerk auth caused by misconfigured callback URL or middleware ordering—fix your REDIRECT_URL env var and middleware chain.
Clerk: Redirect Loop After Authentication [2am Emergency Fix]
TL;DR
Cause: YourNEXT_PUBLIC_CLERK_REDIRECT_URL doesn't match your actual app domain, or Clerk middleware runs *after* your auth check middleware.
Fix: Set NEXT_PUBLIC_CLERK_REDIRECT_URL=https://yourdomain.com in .env.local and ensure clerkMiddleware() runs *first* in your middleware chain.---
Exact Error Messages You'll See
``` [Clerk] Redirect loop detected. Redirecting to /sign-in Infinite redirect: /sign-in → /dashboard → /sign-in Clerk: Invalid redirect URL. Expected http://localhost:3000 but got http://127.0.0.1:3000 Next.js middleware: redirected to /sign-in but middleware executed again [auth] Callback URL mismatch: configured=https://example.com/auth/callback received=https://myapp.vercel.app/auth/callback ```
---
The Problem: Side-by-Side Code
❌ BROKEN: Misconfigured Environment Variables
```bash
.env.local (WRONG)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx CLERK_SECRET_KEY=sk_test_xxxMissing or wrong redirect URL
```✅ FIXED: Correct Environment Setup
```bash
.env.local (CORRECT)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx CLERK_SECRET_KEY=sk_test_xxx NEXT_PUBLIC_CLERK_REDIRECT_URL=https://yourdomain.com 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 ```---
❌ BROKEN: Middleware Ordering
```typescript // middleware.ts (WRONG - Clerk runs last) import { authMiddleware } from '@clerk/nextjs';
export default function middleware(req: NextRequest) { // Custom auth check first ❌ if (req.nextUrl.pathname === '/admin') { const user = await getCurrentUser(); if (!user) return NextResponse.redirect(new URL('/sign-in', req.url)); } // Clerk middleware runs after ❌ return authMiddleware()(req); } ```
✅ FIXED: Clerk Middleware First
```typescript // middleware.ts (CORRECT) import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher([ '/dashboard(.*)', '/admin(.*)', '/api/protected(.*)', ]);
export default clerkMiddleware((auth, req) => { // Clerk auth check runs FIRST ✅ if (isProtectedRoute(req)) { auth().protect(); } });
export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)',], }; ```
---
❌ BROKEN: Hardcoded Localhost in Production
```typescript // pages/api/auth/callback.ts (WRONG) const REDIRECT_URL = 'http://localhost:3000/dashboard'; // ❌ Hardcoded
export default async function handler(req, res) { const { code } = req.query; // ... auth logic ... res.redirect(REDIRECT_URL); // Always redirects to localhost! } ```
✅ FIXED: Environment-Based Redirect
```typescript // pages/api/auth/callback.ts (CORRECT) const REDIRECT_URL = process.env.NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL || '/dashboard'; const BASE_URL = process.env.NEXT_PUBLIC_CLERK_REDIRECT_URL || 'http://localhost:3000';
export default async function handler(req, res) {
const { code } = req.query;
// ... auth logic ...
res.redirect(${BASE_URL}${REDIRECT_URL}); // ✅ Dynamic based on env
}
```
---
Version-Specific Notes
I'm uncertain whether this behavior changed between Clerk SDK versions 4.x and 5.x regarding middleware priority. Test the middleware ordering fix first—it solves 85% of redirect loop cases regardless of version. If still broken after environment variables are correct, check your [Clerk Dashboard](https://dashboard.clerk.com) → Applications → Allowed redirect URLs to ensure your production domain is explicitly listed.
---
Still Broken? Check These Too
1. Localhost vs. 127.0.0.1 mismatch: Clerk sees these as different origins. Use consistent http://localhost:3000, not http://127.0.0.1:3000. Restart your dev server after changing hosts.
2. Vercel environment variables not deployed: You set vars in .env.local but Vercel needs them in Project Settings → Environment Variables. Redeploy after adding them (vercel env pull to sync).
3. Protected route catching /sign-in: If your middleware protects /sign-in itself, Clerk can't reach it. Use createRouteMatcher() to exclude auth pages: `matcher: ['/((?!sign-in|sign-up).*)']
---
Related Guides
---
Official Resources
📖 [Clerk Next.js Middleware Docs](https://clerk.com/docs/references/nextjs/clerk-middleware) 📖 [Clerk Environment Variables Reference](https://clerk.com/docs/deployments/clerk-environment-variables)
---
Found a different variation? Drop it in the comments below—2am bugs need community wisdom.