heydevTalk to founders

Next.jsDevOps

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

Yatish Goel

Yatish Goel

Co-founder, Chief Greed Officer

Published Updated
Black and white film still: duplicated forwarded headers causing a redirect loop, with one ember orange detail

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:
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:

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:

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:

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:

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:

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:

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:

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 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.

---

FAQ

Frequently asked questions

How do I reproduce the redirect loop locally?
Send curl -i http://localhost:3000/ with two x-forwarded-proto: https headers. The duplicated header reproduces the multi-value case reported in Next.js issues #52266 and #54450, including a Location header that points at localhost.
Why do my production redirects point to https://localhost:3000?
Absolute redirects built from host and proto behind a proxy chain pick up the internal hostname when forwarded headers are duplicated or missing. Return relative redirects where possible and build absolute ones from x-forwarded-host.
How do I catch this before users do?
Run a synthetic check that follows redirects and alerts if it sees more than five hops, and log x-forwarded-proto, x-forwarded-host, host, and the redirect Location whenever the proxy returns a redirect.

Sources

  1. 01Next.js issue #52266: x-forwarded-proto arrives as a list
  2. 02Next.js issue #54450: duplicated x-forwarded-proto breaks redirects
  3. 03Next.js docs: proxy file convention

#nextjs #middleware #proxy #vercel #load balancer #redirect loop #x-forwarded-proto

Written by

Yatish Goel

Yatish Goel

Co-founder, Chief Greed Officer

IIT Kanpur / US startup engineer

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.

Keep reading

Related articles