`redirect` vs `permanentRedirect` in Server Components

Both functions stop rendering and send the visitor somewhere else. The difference is the status code, how long clients remember it, and what that means when the route is prerendered.

8 min read

redirect and permanentRedirect both come from next/navigation, and both stop rendering the current route and send the visitor to another URL. The difference between them is one status code, and how long clients are expected to remember it.

redirect answers with a 307, which is temporary. permanentRedirect answers with a 308, which browsers and crawlers may cache indefinitely.

App.tsxApp.tsx
// app/old-profile/page.tsx
import { permanentRedirect } from 'next/navigation'
 
export default async function Page() {
  permanentRedirect('/about')
}

Requesting /old-profile returns a 308 with a location header pointing at /about, and nothing from this component renders. Swapping the import for the other function changes the status to 307 and nothing else.

The difference at a glance

Everything the two functions share is the interesting part. They accept the same arguments, are valid in the same places, and fail in the same ways.

redirectpermanentRedirect
Status code307308
MeaningThe move is temporaryThe old URL is retired
Cached by browsersNoYes, often indefinitely
Request methodPreservedPreserved
Safe to change laterYesHard to undo

Both avoid the older 301 and 302 pair for the same reason config redirects do. Those codes are widely mishandled, and a POST to a redirected path can arrive at the destination as a GET.

The asymmetry in that last row is the one that matters in practice. A 307 you regret disappears the moment you deploy a fix, while a 308 lives in caches you do not control.

What calling one actually does

Neither function returns. Calling either throws a NEXT_REDIRECT error, which Next.js catches above your component and turns into the redirect response.

That is why no return statement is needed. Both are typed as returning never, so TypeScript treats everything after the call as unreachable code.

App.tsxApp.tsx
// app/team/[id]/page.tsx
import { redirect } from 'next/navigation'
import { getTeam } from '@/lib/teams'
 
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const team = await getTeam(id)
  if (!team) redirect('/teams')
  return <h1>{team.name}</h1>
}

When the team is missing, rendering of this segment ends at the redirect call and the browser receives a 307. When it exists, the heading renders normally and the redirect never happens.

Note that the route params are awaited. Request-time APIs are asynchronous in current Next.js, and that applies whether or not the component ends in a redirect.

Keep the call outside try/catch

Because the redirect is delivered as a thrown error, a try/catch block around the call will intercept it. The redirect then silently does not happen, and your catch block reports a failure that never occurred.

App.tsxApp.tsx
// app/account/page.tsx
import { redirect } from 'next/navigation'
import { loadAccount } from '@/lib/account'
 
export default async function Page() {
  const account = await loadAccount().catch(() => null)
  if (!account) redirect('/login')
  return <h1>{account.email}</h1>
}

The failure is handled where it happens, and the redirect sits after it in plain control flow. Anyone without an account reaches the login page, and the redirect is never inside something that could catch it.

Calling the function from inside a catch block is also safe, since the throw escapes upward from there. The dangerous shape is the one where the redirect sits in the try block itself.

A catch-all handler will swallow it

Wrapping the redirect itself in a try block, or catching broadly around a helper that redirects, breaks the redirect without any error surfacing. If a redirect appears to do nothing, check what is catching around it first.

Prerendered routes decide at build time

A Server Component that redirects without reading any request data has nothing request-specific to wait for, so Next.js prerenders the decision. The build marks the route as static, and the redirect response is served from the build output.

That response carries a long shared cache header, which is correct for a genuinely fixed move and wrong for anything conditional. Requesting the route above against a production build shows it.

texttext
HTTP/1.1 308 Permanent Redirect
location: /about
Cache-Control: s-maxage=31536000
x-nextjs-prerender: 1

A permanent redirect on a static route is therefore about as sticky as a redirect gets.

Reading a request-time API changes this. Awaiting cookies makes the route dynamic, the redirect is evaluated per request, and the response comes back with no-store caching instead.

App.tsxApp.tsx
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
 
export default async function Page() {
  const store = await cookies()
  if (!store.get('session')) redirect('/login')
  return <h1>Dashboard</h1>
}

Visitors without the cookie get a 307 to the login page, and visitors with it see the dashboard. Because the route reads cookies, that check runs on every request rather than being frozen at build time.

This is also the reason an auth redirect must never use the permanent function. A cached 308 would send an authenticated visitor to the login page too. Static and dynamic rendering covers what else pushes a route into request-time rendering.

Behavior inside a Server Action

The status codes above describe rendering. Inside a Server Action, both functions behave differently, because the browser is usually not waiting on a plain HTTP response.

When JavaScript is available, redirect and permanentRedirect both perform a client-side navigation rather than an HTTP redirect. When the form is submitted without JavaScript, the response is a 303, which tells the browser to follow up with a GET instead of repeating the POST.

The history behavior also flips. In a Server Action both functions default to pushing a new history entry, while everywhere else they replace the current one. You can override that with the second argument when the default is wrong for your flow.

Redirecting from a Server Action covers the mutation flow in full, including where the call belongs relative to revalidation.

Which one should you use?

The choice between redirect and permanentRedirect is not symmetric, so default to the temporary one. It is correct for every conditional case, and there is no cost to being wrong about it.

Reach for the permanent function only when all three of these hold:

  • The old URL is retired and will not come back.
  • The decision does not depend on the visitor, a cookie, or a session.
  • You want search engines to transfer the old URL's standing to the new one.

If a path mapping is fixed and known ahead of time, consider whether it belongs in the component at all. A config redirect handles it before rendering starts, which is cheaper and easier to audit than a redirect buried in a page.

Rune AI

Rune AI

Key Insights

  • Both functions come from next/navigation and both stop rendering the current segment.
  • redirect sends 307 and permanentRedirect sends 308; both preserve the request method.
  • Each works by throwing NEXT_REDIRECT, so a surrounding try/catch will swallow it.
  • A redirect in a prerendered route is decided at build time and served with a long shared cache header.
  • In a Server Action, both perform a client-side navigation when JavaScript is available.
RunePowered by Rune AI

Frequently Asked Questions

Do I need to return the redirect call?

No. Both functions are typed as returning never, so TypeScript already knows the code after them is unreachable. Writing return redirect() works but adds nothing.

Why does my redirect get swallowed by a catch block?

Because both functions work by throwing a NEXT_REDIRECT error that Next.js catches upstream. A try/catch around the call intercepts that error first, so the redirect never reaches the framework. Call it after the block instead.

Can I call these in a Client Component?

Yes, during render, but not from an event handler. In an event handler use the push method from the useRouter hook, or call a Server Action that redirects.

Which status code does a Server Action redirect use?

Neither 307 nor 308 in the usual case. When JavaScript is available a Server Action performs a client-side navigation, and a progressive enhancement form submission gets a 303 so the browser follows with a GET.

Conclusion

Both functions terminate rendering and send the visitor elsewhere, and both throw to do it. Choose permanentRedirect only when the old URL is retired for good, because a 308 is remembered by browsers and crawlers long after you remove the code. Use redirect for everything conditional, including auth checks.