afterbuild/ops
ERR-589/Clerk · Auth
ERR-589
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.

Last updated 17 April 2026 · 6 min read · By Hyder Shah
Direct answer
Clerk infinite redirect loop is almost always the middleware matcher catching every route including the sign-in page, or a session cookie that does not stick on your production domain. Fix the matcher to exclude 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

middleware.ts
typescript
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};
Replace your middleware.ts with this — excludes auth routes and webhooks, protects only dashboard/settings/private APIs

Deeper fixes when the quick fix fails

01 · Route-by-route protect() with explicit auth.protect()

middleware.ts
typescript
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};
Explicit per-matcher protect() — impossible to loop because sign-in is never tested

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.

dns.txt
text
01# DNS record02clerk.yourdomain.com.  CNAME  frontend-api.clerk.services.

03 · Playwright regression test for the sign-in flow

e2e/auth.spec.ts
typescript
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});
Run on every deploy — catches matcher regressions before they hit real users

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.

AI builder × Clerk redirect loop
BuilderFrequencyPattern
LovableEvery Clerk scaffoldMiddleware matcher catches every route including /sign-in
Bolt.newCommonNEXT_PUBLIC_CLERK_SIGN_IN_URL unset — Clerk falls back to hosted page
v0CommonMissing CNAME for clerk frontend API — cookie drops on Safari
CursorSometimesafterSignInUrl points at a path the middleware also protects
Replit AgentRareWebhooks at /api/webhooks/clerk inside the protected matcher — signature verification fails

Related errors we fix

Stop Clerk redirect loop recurring in AI-built apps

Still stuck with Clerk redirect loop?

Emergency triage · $299 · 48h turnaround
We restore service and write the root-cause report.
start the triage →

Clerk redirect loop questions

Why does Clerk keep redirecting me between /sign-in and /dashboard?+
The loop happens when your middleware treats /sign-in as a protected route. The user lands on /sign-in, the middleware sees no session, redirects to /sign-in (which is already /sign-in), and Clerk then honors the afterSignInUrl and pushes to /dashboard. But your client code reads no session cookie yet, so it triggers another redirect to /sign-in. Fix by excluding auth routes from the middleware matcher.
What is the correct clerkMiddleware matcher for a Next.js App Router app?+
Protect everything except public assets and auth routes. A safe matcher is `['/((?!.*\..*|_next|sign-in|sign-up|api/webhooks).*)', '/', '/(api|trpc)(.*)']`. This excludes files with extensions, /_next internals, the sign-in and sign-up paths, and the Stripe/Clerk webhook endpoints. Everything else runs through the middleware, which still allows auth.protect() per route.
Why does the session cookie not set on my custom domain?+
Clerk sets cookies scoped to the instance's frontend API domain. In production you must set a CNAME (clerk.yourdomain.com) so the cookie is first-party on your app origin. Without the CNAME, Safari's Intelligent Tracking Prevention drops the cookie after navigation, the session looks signed-in on the first page and signed-out on the second, producing a redirect loop. Configure the CNAME in Clerk Dashboard under DNS and redeploy.
What does NEXT_PUBLIC_CLERK_SIGN_IN_URL do?+
It tells Clerk's React components where to redirect unauthenticated users. If it is unset or points to a path your middleware protects, Clerk sends users there and the middleware bounces them back. Set it to /sign-in (matching your actual <SignIn /> page) and confirm the same path is excluded from the middleware matcher. AI scaffolds often omit this variable and fall back to the hosted Clerk page, which breaks on custom domains.
How long does a Clerk redirect loop fix take?+
Fifteen to thirty minutes for the middleware matcher fix. One hour if you also need to configure the custom frontend API CNAME and redeploy. Our Emergency Triage at $299 includes the matcher audit, CNAME setup, and a Playwright test that walks the sign-in flow to make sure it does not regress. Auth flows are hard to debug manually; automate it.
Next step

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.

About the author

Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.

Sources