Next.js Proxy Redirect Loops Behind a Load Balancer (2026)

Yatish Goel

Yatish Goel

Co-Founder & CTO

February 5, 2026
#nextjs#middleware#proxy#vercel#load balancer#redirect loop#x-forwarded-proto

If your Next.js app is sitting behind a load balancer, CDN, ingress, or any kind of “helpful” proxy, you can hit a class of bugs that feels like gaslighting.

Everything works locally.

Staging works.

Then production turns into an infinite redirect loop, or worse, your redirects start pointing to `https://localhost:3000/...` like your Vercel deployment suddenly moved into your laptop.

This post is about that exact mess. The root cause is usually some combo of:

- duplicated `x-forwarded-proto` headers (`https,http` or `http,http`)

- wrong `host` (or missing `x-forwarded-host`)

- redirects built from `req.nextUrl` in Proxy (formerly Middleware)

- trailing slash redirects fighting your upstream

And in 2026, it’s extra fun because Next.js has shifted the “middleware” convention to **Proxy** (`proxy.ts`), so people copy older snippets and then wonder why edge behavior is weird.

I’ll show you what’s happening, how to reproduce it in 30 seconds, and the fix patterns that actually stick.

The symptom checklist (aka “why is my app stuck on 307?”)

You are probably here because:

1. You see `ERR_TOO_MANY_REDIRECTS` in the browser.

2. Your logs show the same path being hit over and over.

3. Your Proxy code has something like:

```ts

import { NextRequest, NextResponse } from 'next/server'

export function proxy(req: NextRequest) {

if (req.headers.get('x-forwarded-proto') !== 'https') {

return NextResponse.redirect(`https://${req.headers.get('host')}${req.nextUrl.pathname}`)

}

return NextResponse.next()

}

```

4. Or you are doing a “force canonical host” redirect.

5. Or you are proxying `/blog/*` to WordPress and the trailing slash is starting a war.

If any of those ring a bell, keep reading.

What the research says (and what it does not say)

This isn’t theoretical. Two GitHub issues capture the ugly details:

- Next.js issue #52266: `x-forwarded-proto` showing up as `http,http` or `https,http`, causing a redirect loop when code checks for exact equality with `https`.

- Next.js issue #54450: duplicated `x-forwarded-proto: https` can break redirects and create a `location: https://localhost:3000/test` header.

Also, Next.js docs (Feb 2026) note that the middleware file convention is deprecated and renamed to **proxy**. That matters because a lot of old blog posts still say “middleware.ts”, and the runtime and defaults have shifted.

Sources:

- https://github.com/vercel/next.js/issues/52266

- https://github.com/vercel/next.js/issues/54450

- https://nextjs.org/docs/app/api-reference/file-conventions/proxy

Why this happens: the “forwarded headers are not a single value” problem

Most snippets assume `x-forwarded-proto` is a single string: `http` or `https`.

Reality behind proxies: it can be a comma-separated list.

Example from the issue report:

- You expect `https`

- You get `http,http` or `https,http`

If your code does `!== 'https'`, it will always redirect.

And because the redirect request comes through the same proxy chain again, you get the loop.

This is the same category of bug as “trusting `req.url` behind a proxy in Express without `trust proxy`”. Different stack, same pain.

Fast reproduction in 30 seconds

You can simulate the duplicated header issue locally with curl:

```bash

curl -i 'http://localhost:3000/' \

-H 'x-forwarded-proto: https' \

-H 'x-forwarded-proto: https'

```

According to the #54450 report, the redirect `Location` can turn into an absolute URL pointing at `https://localhost:3000/test`.

That is not what you want in prod.

Proxy (ex-Middleware) gotchas in 2026

A few things that trip teams up right now:

1) Proxy runs before a lot of your app logic

Proxy executes before routes render. If Proxy redirects, your app never even gets a chance.

So any “just redirect to /login” logic must be extremely careful about matchers, or you will redirect your login page too. That creates a loop that looks identical to the forwarded-header loop, but it’s your matcher.

2) Trailing slash redirects can fight your upstream

If you proxy `/blog/*` to WordPress, WordPress loves trailing slashes.

Vercel and Next.js often normalize URLs. If Next strips the slash and WordPress adds it back, welcome to infinite redirects.

The docs mention flags like `skipTrailingSlashRedirect`. If you are doing mixed routing (Next app + upstream app), those flags stop a lot of nonsense.

3) People build absolute URLs when they do not need to

If you can return a relative redirect (`/login`, `/test`, `/settings`), do it.

Absolute redirects are where `host`, `proto`, and proxy chains show up to ruin your day.

Fix pattern #1: parse forwarded headers like a grown-up

Treat forwarded headers as potentially multi-valued.

Here’s a safe helper that handles `https,http` and weird spacing:

```ts

function getForwardedProto(req: NextRequest) {

const raw = req.headers.get('x-forwarded-proto') || ''

// Could be \"https,http\". Take the first value.

return raw.split(',')[0].trim().toLowerCase()

}

```

Then:

```ts

const proto = getForwardedProto(req)

if (process.env.NODE_ENV === 'production' && proto !== 'https') {

// redirect

}

```

Does it feel silly? Yes.

Does it stop the loop? Also yes.

Fix pattern #2: stop building absolute redirect URLs (most of the time)

If your goal is just to send the user to a route in your own app, do:

```ts

const url = req.nextUrl.clone()

url.pathname = '/login'

url.searchParams.set('next', req.nextUrl.pathname)

return NextResponse.redirect(url)

```

But even that can produce weird absolute redirects depending on environment.

If you do not need the full origin, return a relative redirect:

```ts

return NextResponse.redirect(new URL('/login', req.url))

```

Or use `Response.redirect('/login', 307)` style patterns where available.

The fewer times you stitch strings with `host` and `proto`, the fewer “why is localhost in production” nights you’ll have.

Fix pattern #3: use x-forwarded-host (and friends) when you must build absolute URLs

Sometimes you really do need absolute redirects:

- canonical host enforcement (`example.com` vs `www.example.com`)

- redirecting to a different subdomain (`app.` to `www.`)

- auth flows (some IdPs are picky)

When that happens, do not assume `host` is correct.

Try this order:

1. `x-forwarded-host`

2. `host`

And if you are behind multiple proxies, make sure your infra is not appending duplicates in the first place.

Example:

```ts

function getForwardedHost(req: NextRequest) {

const raw = req.headers.get('x-forwarded-host') || req.headers.get('host') || ''

return raw.split(',')[0].trim()

}

```

Then build `https://${host}${path}`.

Fix pattern #4: matchers that do not shoot you in the foot

A lot of redirect loops are self-inflicted.

You set a matcher like `['/:path*']` and then you redirect unauthenticated users to `/login`.

Proxy runs on `/login` too.

Proxy sees “unauthenticated” and redirects to `/login` again.

Loop.

Be explicit:

- exclude `/login`

- exclude `/api/auth/*`

- exclude `/_next/*`

- exclude static files

In 2026, you should treat your Proxy matcher like a firewall rule set, not a casual regex.

Fix pattern #5: stop letting trailing slashes decide your fate

If your upstream enforces trailing slashes and Next removes them (or the other way), pick a single policy.

Two practical options:

1. Make Next keep the slashes for that subtree (`/blog/*`).

2. Make the upstream stop redirecting (harder if it’s WordPress).

If you are doing “hybrid hosting” (Next app + external blog), set the flags that disable automatic trailing slash redirects and normalization for that route set.

This is the kind of issue that wastes days and teaches you nothing. So fix it once and move on.

The boring, real-world cost of this bug

Let’s talk dollars, because founders care.

When we get pulled into one of these cases, it’s usually after:

- 2 to 5 hours of “it works locally” debugging

- 1 broken production deploy

- a bunch of angry users stuck on a redirect loop

A typical rescue looks like:

- 1 to 2 hours: reproduce with headers and logs

- 1 to 3 hours: fix Proxy logic and matchers

- 1 hour: harden and add monitoring (so you see it next time)

So you are looking at **3 to 6 hours** of senior dev time.

In US/UK/EU rates, that’s often **$450 to $1,500**.

And if you deploy multiple times while guessing, add more.

This is why I hate “clever” middleware snippets. They are cheap up front, expensive later.

A minimal “production-safe” Proxy snippet (2026)

Here’s a baseline I’m comfortable shipping:

```ts

import { NextRequest, NextResponse } from 'next/server'

function firstHeaderValue(v: string | null) {

if (!v) return ''

return v.split(',')[0].trim().toLowerCase()

}

function getProto(req: NextRequest) {

return firstHeaderValue(req.headers.get('x-forwarded-proto'))

}

function getHost(req: NextRequest) {

const xfHost = req.headers.get('x-forwarded-host')

const host = req.headers.get('host')

return (xfHost || host || '').split(',')[0].trim()

}

export const config = {

// Do NOT run on /login or Next internals

matcher: ['/((?!_next|api|login).*)'],

}

export function proxy(req: NextRequest) {

// Only force https in production

if (process.env.NODE_ENV === 'production') {

const proto = getProto(req)

if (proto && proto !== 'https') {

const host = getHost(req)

const url = new URL(req.nextUrl.pathname + req.nextUrl.search, `https://${host}`)

return NextResponse.redirect(url, 308)

}

}

return NextResponse.next()

}

```

Is it perfect? No.

Is it harder to break? Yes.

Monitoring: how to catch this before users do

Two cheap checks:

1. Add a synthetic ping from a US region that follows redirects and alerts if it sees >5 redirects.

2. Log `x-forwarded-proto`, `x-forwarded-host`, `host`, and the redirect `Location` when Proxy returns a redirect.

This is not “observability”, it’s just basic self-respect.

Takeaways

- `x-forwarded-proto` can be multi-valued. Do not compare it like a single token.

- Prefer relative redirects. Absolute redirects bring header drama.

- Proxy (ex-Middleware) runs early. Matchers matter.

- Trailing slash wars are real, especially when you proxy to WordPress.

- You can fix this in an afternoon, but only if you stop guessing and reproduce it with headers.

If you’re a founder in the US/UK and your Next.js app is randomly redirect-looping after a deploy, it’s almost never “Next.js is broken”. It’s your proxy chain.

And yeah, it’s annoying.

---

Related reading

Yatish Goel

Yatish Goel

Co-Founder & CTO

US Startup ExperienceIIT Kanpur

Full-stack architect with US startup experience and an IIT Kanpur degree. Yatish drives the technical vision at HeyDev, designing robust architectures and leading development across web, mobile, and AI projects.

Related Articles