ERR_TOO_MANY_REDIRECTS — the browser bounces between /sign-in and /dashboard indefinitely
appears when:After deploying a Clerk-protected Next.js app to a custom domain, or after adding middleware that protects the sign-in page itself
Clerk infinite redirect loop
Clerk loops when the middleware treats /sign-in as protected, or when the session cookie does not set on your production domain. Both are single-file fixes.
sign-in and sign-up, set NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in, and add the CNAME forclerk.yourdomain.com in production.Quick fix for Clerk redirect loop
01// middleware.ts — exclude auth routes from clerkMiddleware matcher02import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";03 04const isProtectedRoute = createRouteMatcher([05 "/dashboard(.*)",06 "/settings(.*)",07 "/api/private(.*)",08]);09 10export default clerkMiddleware(async (auth, req) => {11 if (isProtectedRoute(req)) {12 await auth.protect();13 }14});15 16export const config = {17 matcher: [18 // skip static files, _next, auth routes, and webhook endpoints19 "/((?!_next|.*\\..*|sign-in|sign-up|api/webhooks).*)",20 "/",21 "/(api|trpc)(.*)",22 ],23};Deeper fixes when the quick fix fails
01 · Route-by-route protect() with explicit auth.protect()
01// middleware.ts — granular protect() rather than global matcher02import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";03 04const isDashboardRoute = createRouteMatcher(["/dashboard(.*)"]);05const isAdminRoute = createRouteMatcher(["/admin(.*)"]);06const isPrivateApi = createRouteMatcher(["/api/private(.*)"]);07 08export default clerkMiddleware(async (auth, req) => {09 if (isAdminRoute(req)) {10 await auth.protect({ role: "admin" });11 } else if (isDashboardRoute(req) || isPrivateApi(req)) {12 await auth.protect();13 }14 // all other routes are public — no redirect triggered15});16 17export const config = {18 matcher: [19 "/((?!_next|.*\\..*|sign-in|sign-up|api/webhooks).*)",20 "/(api|trpc)(.*)",21 ],22};02 · Custom frontend API CNAME for production cookies
In Clerk Dashboard → DNS, copy the CNAME target. In your DNS provider add a CNAME record for clerk.yourdomain.com pointing at that target. Save. Wait for propagation, then redeploy your app. This makes Clerk's cookie first-party on your domain and fixes Safari ITP dropping the session.
01# DNS record02clerk.yourdomain.com. CNAME frontend-api.clerk.services.03 · Playwright regression test for the sign-in flow
01// e2e/auth.spec.ts — asserts no redirect loop on sign-in02import { test, expect } from "@playwright/test";03 04test("sign-in does not loop", async ({ page }) => {05 await page.goto("/dashboard");06 // clerk should redirect exactly once to /sign-in07 await page.waitForURL(/\/sign-in/, { timeout: 5000 });08 expect(page.url()).toContain("/sign-in");09 // fill and submit10 await page.fill('input[name="identifier"]', process.env.TEST_USER_EMAIL!);11 await page.click('button[type="submit"]');12 // land on dashboard with no intermediate loop13 await page.waitForURL(/\/dashboard/, { timeout: 10000 });14});Why AI-built apps hit Clerk redirect loop
Clerk uses a middleware-at-the-edge pattern. Every request hits middleware.ts, which checks the session cookie. If the route is marked protected and there is no session, the middleware redirects to the sign-in URL (default /sign-in). This is correct for protected pages — but catastrophic if the matcher is too broad and captures /sign-in itself. The user lands on /sign-in, middleware sees no session, redirects to /sign-in, which the browser follows back to the same page. After a few bounces Chrome shows ERR_TOO_MANY_REDIRECTS and stops.
AI scaffolds from Lovable, Bolt, and Cursor typically paste the Clerk quickstart into the middleware matcher as is, and the quickstart used to match everything except static assets. Newer Clerk docs recommend a stricter matcher that excludes sign-in and sign-up explicitly, plus any webhook paths that must not require auth (Stripe, Clerk, SendGrid). If the AI-generated code pre-dates that guidance, the loop is guaranteed.
The second common cause is the production cookie. Clerk sets its session cookie scoped to the Clerk frontend API domain (like accounts.dev in development). In production you must point a CNAME — usually clerk.yourdomain.com — at the Clerk-provided target. This makes the cookie first-party on your app origin. Without the CNAME, Safari's ITP drops the cookie after navigation, your dashboard page sees no session, middleware redirects back to sign-in, the loop begins.
The third source is a mismatch between NEXT_PUBLIC_CLERK_SIGN_IN_URL and your actual sign-in page. If the env var points to /sign-in but the page lives at /login, or vice versa, Clerk's React components redirect to a URL your app does not serve. Middleware hits a 404, or rewrites to the real path and triggers a second redirect. Always grep your code for every place that reads this env var and make sure it matches the file at app/sign-in/[[...sign-in]]/page.tsx.
Clerk redirect loop by AI builder
How often each AI builder ships this error and the pattern that produces it.
| Builder | Frequency | Pattern |
|---|---|---|
| Lovable | Every Clerk scaffold | Middleware matcher catches every route including /sign-in |
| Bolt.new | Common | NEXT_PUBLIC_CLERK_SIGN_IN_URL unset — Clerk falls back to hosted page |
| v0 | Common | Missing CNAME for clerk frontend API — cookie drops on Safari |
| Cursor | Sometimes | afterSignInUrl points at a path the middleware also protects |
| Replit Agent | Rare | Webhooks at /api/webhooks/clerk inside the protected matcher — signature verification fails |
Related errors we fix
Stop Clerk redirect loop recurring in AI-built apps
- →Never match /sign-in or /sign-up in your middleware config — use createRouteMatcher for protected paths explicitly.
- →Configure the Clerk frontend API CNAME in production before shipping to a custom domain.
- →Set NEXT_PUBLIC_CLERK_SIGN_IN_URL in every environment and confirm it matches a real page.
- →Always exclude webhook endpoints (/api/webhooks/*) from the middleware matcher.
- →Write a Playwright test that walks sign-in and sign-out in CI for every deploy.
Still stuck with Clerk redirect loop?
Clerk redirect loop questions
Why does Clerk keep redirecting me between /sign-in and /dashboard?+
What is the correct clerkMiddleware matcher for a Next.js App Router app?+
Why does the session cookie not set on my custom domain?+
What does NEXT_PUBLIC_CLERK_SIGN_IN_URL do?+
How long does a Clerk redirect loop fix take?+
Ship the fix. Keep the fix.
Emergency Triage restores service in 48 hours. Break the Fix Loop rebuilds CI so this error cannot ship again.
Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.