Clerk: redirect loop after authentication [2026 fix]

Redirect loop caused by misconfigured afterSignInUrl or missing middleware—fix by setting absolute URLs and ensuring middleware runs before redirects.

Clerk: Redirect Loop After Authentication [2am Fix]

TL;DR

Cause: afterSignInUrl is relative or middleware isn't intercepting auth state before your app redirects. Fix: Use absolute URLs in Clerk config and ensure clerkMiddleware() runs before all route handlers.

---

Real Console Error Messages

Look for these exact patterns in your browser/server logs:

``` ERROR: Redirect loop detected: http://localhost:3000/auth/callback redirecting to http://localhost:3000/auth/callback ```

``` WARNING: ClerkProvider: User authenticated but sessionId is stale. Forcing re-authentication. ```

``` ERROR [Clerk]: afterSignInUrl "/dashboard" is not an absolute URL. Expected format: "https://example.com/dashboard" ```

``` TypeError: Cannot read property 'isLoaded' of undefined at useAuth hook ```

``` POST /api/auth/callback 308 (redirect loop) - middleware did not process request ```

---

The Problem: Code Comparison

❌ BROKEN CODE

middleware.ts (missing or misconfigured): ```typescript // This doesn't exist or runs too late export default function middleware(request: NextRequest) { return NextResponse.next(); }

export const config = { matcher: ["/dashboard/:path*"], // Too narrow! }; ```

app.tsx (relative afterSignInUrl): ```typescript <ClerkProvider publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY} afterSignInUrl="/dashboard" // ❌ RELATIVE URL > <App /> </ClerkProvider> ```

route.ts (auth callback): ```typescript export async function GET(req: NextRequest) { const { isLoaded, userId } = useAuth(); // ❌ Hook in server component if (!userId) return NextResponse.redirect("/"); return NextResponse.json({ success: true }); } ```

✅ FIXED CODE

middleware.ts (correct): ```typescript import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server';

const isProtectedRoute = createRouteMatcher([ '/dashboard(.*)', '/api/protected(.*)', ]);

export default clerkMiddleware((auth, req) => { if (isProtectedRoute(req)) { auth().protect(); } return NextResponse.next(); });

export const config = { matcher: [ '/((?!.*\\.[\\w]+$|_next).*)', // ✅ Matches ALL routes except static '/api/(.*)', ], }; ```

app.tsx (absolute afterSignInUrl): ```typescript const clerkPubKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; const signInUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL || ${process.env.NEXT_PUBLIC_APP_URL}/sign-in;

<ClerkProvider publishableKey={clerkPubKey} afterSignInUrl={${process.env.NEXT_PUBLIC_APP_URL}/dashboard} // ✅ ABSOLUTE afterSignOutUrl={${process.env.NEXT_PUBLIC_APP_URL}/} > <App /> </ClerkProvider> ```

route.ts (server-side, no hooks): ```typescript import { auth } from '@clerk/nextjs/server'; import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) { const { userId } = await auth(); // ✅ Async function, not hook if (!userId) { return NextResponse.redirect( new URL('/sign-in', process.env.NEXT_PUBLIC_APP_URL) ); } return NextResponse.json({ success: true }); } ```

.env.local (required variables): ```bash NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_... NEXT_PUBLIC_APP_URL=https://localhost:3000 # or your production domain ```

---

Why This Happens

1. Relative URLs: Clerk can't build absolute redirect URLs without your app's base URL, causing auth callback to loop 2. Missing middleware: Auth state isn't established before your app tries to check it 3. Hook misuse: Using useAuth() in server components breaks the auth flow 4. Narrow matcher: Middleware skips auth callback routes 5. Stale session: User is authenticated but ClerkProvider hasn't synced state

---

Still Broken? Check These Too

1. [CORS/Domain Mismatch](/?guide=clerk-cors-errors): Verify NEXT_PUBLIC_APP_URL matches your exact domain (including protocol). Localhost ≠ 127.0.0.1.

2. [Multiple ClerkProvider Instances](/?guide=clerk-multiple-providers): Ensure only ONE <ClerkProvider> wraps your app. Check _app.tsx, layout.tsx, and any custom auth wrappers.

3. [Session Token Expired](/?guide=clerk-session-refresh): Add <ClerkLoaded> wrapper around protected components. Session might be valid but provider hasn't hydrated yet.

---

Version Note

I'm confident about Clerk SDK v5+ behavior here. If you're on v4 or earlier, clerkMiddleware() signature differs—check official docs below. Comment if you hit version-specific issues!

---

Resources

  • [Clerk Next.js Middleware Docs](https://clerk.com/docs/references/nextjs/clerk-middleware)
  • [Clerk Environment Variables](https://clerk.com/docs/deployments/clerk-environment-variables)
  • [Auth State Setup Guide](https://clerk.com/docs/nextjs/get-started-with-nextjs)
  • ---

    Found a different variation? Drop it in the comments—2am bugs are always weird, and your fix might save someone else's night.

    🔥 0d
    LIVE
    ↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising •↑ Resend loved by devs •↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising
    DEVELOPER PAIN RADAR // Loading...

    Developers complain.
    Opportunities appear.

    We track what developers are struggling with today — and what opportunities that creates.

    —
    guides today
    —
    avg happiness
    🔥 Pain
    📖 Guides
    🔭 Explore
    👤 Mine
    🔥 Pain Radar — rage scores today
    ↗ share
    💡 Opportunity Feed — pain = market gap
    📈 Tool Momentum
    all scores →
    📖 Latest Guide
    all guides →
    📖 All Guides
    📊 Tool Scores
    + Submit
    📰 Hacker News
    ➕ Submit a Tool
    ← back