Type something to search...
Next.js permanentRedirect

Next.js permanentRedirect

Every framework that handles routing eventually needs an answer to one specific question: "this URL used to point somewhere, and now it permanently points somewhere else — how do I tell the browser and every search engine crawler that follows this link, once and for all?" In Next.js, that answer is permanentRedirect, a small function from next/navigation that looks almost identical to its sibling redirect, but carries a different, stronger promise: this move is not coming back.

It's easy to treat permanentRedirect and redirect as interchangeable, since they share nearly the same call signature and both work by throwing a special error under the hood. But the distinction between a 307/303 (temporary) and a 308 (permanent) HTTP status code is not cosmetic — it changes how browsers cache the redirect, how search engines transfer ranking signal from the old URL to the new one, and how confidently you can tell your team "yes, we can delete the old route now." This article covers exactly how permanentRedirect behaves, where it fits next to redirect, notFound, and config-level redirects, and the mistakes that trip people up in practice.

What Problem This Actually Solves

Imagine you're running a blog (like this one) and you decide to restructure your URLs — maybe you're moving from /blog/post-slug to /articles/post-slug, or consolidating three near-duplicate landing pages into one canonical page. The old URLs still exist in search results, in other people's backlinks, in bookmarks. You can't just delete them; you need to tell every visitor and every crawler, definitively, "what you're looking for now lives here instead."

A permanent redirect (HTTP 301 historically, 308 in Next.js's implementation) does exactly that. Unlike a temporary redirect, it tells the browser it's safe to cache the redirect and go straight to the new URL next time, and it tells search engines to transfer the old page's accumulated ranking signal to the new URL rather than treating them as two separate pages competing against each other.

permanentRedirect is how you express that intent directly from your route's rendering logic — inside a Server Component, a Route Handler, or a Server Function — rather than only being able to configure it statically in next.config.js.

Signature and Parameters

permanentRedirect(path, type);
ParameterTypeDescription
pathstringThe URL to redirect to. Can be relative (/new-page) or absolute (https://example.com/new-page).
type'replace' (default) or 'push' (default in Server Actions)Whether the redirect replaces the current browser history entry or pushes a new one.

That second parameter is easy to overlook, but it matters more than it looks. Outside of Server Actions, permanentRedirect defaults to replace — meaning if a user clicks the browser's back button after being redirected, they don't land back on the URL that just redirected them (which would immediately redirect them forward again, an infinite ping-pong). Inside a Server Action, the default flips to push, because the mental model there is closer to "the user took an action, and now they've navigated to a result," which is a history-worthy event.

You can override either default explicitly using the RedirectType enum:

import { permanentRedirect, RedirectType } from "next/navigation";

permanentRedirect("/new-location", RedirectType.replace);
// or
permanentRedirect("/new-location", RedirectType.push);

Inside a plain Server Component, the type parameter has no effect at all — there's no client-side history stack to manipulate yet, since the redirect happens before the page ever renders in the browser.

How It Actually Behaves at the Protocol Level

This is the part worth understanding properly, because it's where permanentRedirect earns its name and where its behavior diverges based on context:

Full-page navigation (typical case): the server responds with a real HTTP 308 status code and a Location header. The browser follows it transparently, and because 308 explicitly means "permanently moved, and preserve the original request method," a POST request that hits a permanently-redirected URL will still be sent as a POST to the new location — unlike a 302, which historically got reinterpreted as a GET by many clients.

Streaming context: if the redirect fires partway through a streamed response (React already started sending HTML to the browser), Next.js can't rewrite the HTTP status code anymore — the headers are long gone. Instead, it inserts a <meta http-equiv="refresh"> tag into the already-streaming HTML, which tells the browser to navigate client-side to the new URL. Functionally similar result, different mechanism.

Inside a Server Action: when JavaScript is available in the browser, permanentRedirect performs a client-side navigation using the router rather than a hard reload — faster, and it preserves client-side state where possible. For old-school progressive-enhancement form submissions (no JS, a real <form action> POST), it falls back to serving a 303 "See Other" response, which is the correct status code for "the result of this POST lives at a different URL, go fetch it with GET."

That three-way behavior — 308 for normal navigation, a meta-refresh tag mid-stream, 303 for no-JS form submissions — is entirely handled for you. You call one function; Next.js picks the right mechanism for the context it's running in.

A Working Example

The textbook use case is redirecting away from a resource that has permanently moved, discovered while fetching data for a page:

// app/team/[id]/page.tsx
import { permanentRedirect } from "next/navigation";

async function fetchTeam(id: string) {
  const res = await fetch(`https://api.example.com/teams/${id}`);
  if (res.status === 410) {
    // 410 Gone: this team was merged into another, we know exactly where
    return { movedTo: (await res.json()).newTeamId };
  }
  if (!res.ok) return undefined;
  return res.json();
}

export default async function TeamProfile({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const team = await fetchTeam(id);

  if (team?.movedTo) {
    permanentRedirect(`/team/${team.movedTo}`);
  }

  if (!team) {
    // Resource genuinely doesn't exist — this is a 404, not a redirect
    // (use notFound() here instead)
  }

  return <div>{/* render team profile */}</div>;
}

Notice the distinction drawn in that last comment — it's a common source of confusion, covered below.

permanentRedirect vs. redirect vs. notFound vs. next.config.js redirects

Next.js gives you four different tools that all sound vaguely related, and picking the wrong one is a frequent, low-severity but SEO-relevant mistake:

ToolStatus codeWhen to use it
redirect()307 (302 semantics)The move is temporary, conditional, or you're not fully sure yet — A/B tests, maintenance-mode redirects, auth gates sending unauthenticated users to /login.
permanentRedirect()308 (301 semantics)The move is final. The old URL will never come back, and you want search engines to consolidate ranking signal onto the new URL.
notFound()404The resource genuinely doesn't exist and isn't going anywhere else — there's no "correct" URL to send the visitor to.
redirects in next.config.js307 or 308 (configurable)The mapping is known statically at build/deploy time (not dependent on a database lookup or request-time logic) — a permanent site-wide restructure, a legacy path cleanup.

The rule of thumb: if you can express the redirect as a static, unconditional rule ahead of time, put it in next.config.js — it runs earlier in the request lifecycle (before your page code even executes) and is easier to audit as a single source of truth. Reach for permanentRedirect() inside a component specifically when the decision depends on something you can only know at request time — a database lookup that reveals a resource was merged or renamed, a redirect map stored in a CMS, or logic based on the authenticated user.

The try/catch Gotcha

This one catches nearly everyone at least once. permanentRedirect works by throwing a special NEXT_REDIRECT error internally, which Next.js's rendering machinery intercepts and turns into the actual redirect response. That means if you call it inside a try block, your own catch will intercept that internal error before Next.js ever sees it — silently swallowing the redirect and usually surfacing a confusing unrelated error instead (or, worse, doing nothing at all and rendering the rest of the component).

// Wrong — the redirect gets caught by your own try/catch
async function handler() {
  try {
    const data = await fetchSomething();
    if (data.moved) {
      permanentRedirect("/new-location"); // never actually redirects
    }
  } catch (error) {
    console.error(error); // swallows the NEXT_REDIRECT throw too
  }
}

// Right — call it outside the try block
async function handler() {
  let data;
  try {
    data = await fetchSomething();
  } catch (error) {
    console.error(error);
    return;
  }
  if (data.moved) {
    permanentRedirect("/new-location");
  }
}

This is exactly why the docs explicitly call out Server Actions and Route Handlers as places to be careful about try/catch placement — those are exactly the contexts where wrapping broad chunks of logic in a single try block is tempting.

You Don't Need to return It — But It Reads Better If You Do

Because permanentRedirect is typed to return never in TypeScript (the type-system equivalent of "this function does not return control to the caller"), you technically don't need to write return permanentRedirect(...). Execution stops there regardless. That said, many teams write return permanentRedirect(...) anyway as a readability convention — it signals to the next person reading the function that nothing after this line will ever execute, without them needing to know the never type trick.

Common Mistakes

Using it for something that isn't actually permanent. The most common misuse is reaching for permanentRedirect out of habit or because "permanent" sounds more authoritative, when the situation is actually conditional or temporary — an auth gate, a feature flag, a maintenance page. If there's any chance the redirect target will change again, use redirect() instead. Once you tell browsers and search engines something is permanent, undoing that is slow; cached 308s and reassigned search rankings don't reverse themselves quickly.

Confusing it with router.push() from next/navigation's client-side hook. permanentRedirect (and redirect) are server-side APIs meant for Server Components, Route Handlers, and Server Functions. The client-side equivalent for imperative navigation from event handlers in a Client Component is the useRouter() hook's push/replace methods, which don't carry any HTTP status code semantics at all — they're pure client-side history manipulation.

Expecting it to work for resources that don't exist. If there's no sensible destination to send the visitor to — the content was deleted outright, not moved — the correct tool is notFound(), which renders your not-found.js boundary with a 404 status. Redirecting a genuinely-deleted resource to your homepage or a generic error page is worse for both users and SEO than a clean 404.

Forgetting that streaming changes the mechanism. If you're testing redirect behavior by inspecting HTTP status codes in a network tab and the response instead looks like a 200 with a meta-refresh tag buried in the HTML, that's not a bug — it means the redirect fired after streaming had already started. The end-user behavior is the same; the wire format just differs based on timing.

Key Takeaways

QuestionAnswer
What status code does it send?308 (Permanent Redirect) for normal navigation; 303 for no-JS form submissions; a meta-refresh tag if already streaming.
Where can I call it?Server Components, Client Components, Route Handlers, and Server Functions.
What's the default history behavior?replace everywhere except inside Server Actions, where it defaults to push.
How does it interact with try/catch?Call it outside any try block — it throws internally, and your own catch will otherwise swallow that throw.
When should I use redirect() instead?When the move is temporary, conditional, or you're not certain it's final.
When should I use next.config.js redirects instead?When the mapping is static and known ahead of time, rather than dependent on request-time logic.
When should I use notFound() instead?When the resource is gone entirely and there's no meaningful destination to redirect to.

permanentRedirect is a small function with an outsized amount of nuance packed into two words: the status code it sends, and the promise that status code makes to browsers and search engines. Reach for it deliberately — once you've told the internet a move is permanent, you want to actually mean it.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper