Most guides to PHP redirects tell you to open the old file and add a header call at the top. On a plain PHP site that works.

On WordPress it usually does not, because the thing you are trying to redirect is not a file. Posts and pages live in the database and get routed through index.php, so there is no old-page.php to edit.

This covers how PHP redirects actually work, the WordPress-native functions that replace them, when a raw PHP redirect is still the right call, and what to do once you have enough redirects to cause a different problem.

How a PHP redirect works

A redirect is an HTTP response that tells the browser to go somewhere else. In PHP you send that with the header function.

<?php
header("Location: https://example.com/new-page/", true, 301);
exit;

Three parts matter here.

The Location header names the destination. Use an absolute URL. Relative paths work in most browsers but break in enough edge cases that they are not worth the risk.

The 301 is the status code, and the third argument is the cleanest way to set it. Without it you send a 302, which tells search engines the move is temporary and the old URL should stay indexed.

The exit is not optional. Without it PHP carries on executing the rest of the script after sending the header, which can output the very content you were trying to redirect away from.

Why it fails with headers already sent

The most common error with this code is a warning that headers were already sent.

Headers have to go before any output. A single blank line before your opening PHP tag, a stray space after a closing tag, or an echo earlier in the file all count as output, and once one byte has gone to the browser the header call is too late.

If you hit that error, look for whitespace outside the PHP tags in the file and in anything it includes.

On WordPress, use the WordPress functions

WordPress wraps all of this, and the wrapper does things the raw header call does not.

wp_redirect( 'https://example.com/new-page/', 301 );
exit;

There is a safer version, and it is the one to reach for by default.

wp_safe_redirect( 'https://example.com/new-page/', 301 );
exit;

The difference is that wp_safe_redirect only allows destinations on your own host unless you explicitly permit others. If any part of the destination comes from user input, a query string, or form data, that restriction is what stops your site being used to bounce people somewhere hostile.

Both still need the exit after them, for the same reason the raw version does.

Where the code actually goes

This is the part the generic tutorials skip, and it is the only part specific to WordPress.

There is no file for a post, so the redirect has to hook into the request instead. The template_redirect hook fires after WordPress has worked out what page was requested and before anything renders.

add_action( 'template_redirect', function () {
    if ( is_page( 'old-page-slug' ) ) {
        wp_safe_redirect( home_url( '/new-page/' ), 301 );
        exit;
    }
} );

Put that in a site-specific plugin rather than your theme’s functions.php, or the redirect disappears the next time you change themes.

For a whole old domain or a pattern of URLs, this is the wrong tool entirely. Server-level rules in .htaccess or your Nginx config are faster, because they never load PHP at all.

When a PHP redirect is the right choice

Given all that, there are still cases where writing the redirect in code is correct.

Conditional logic is the clearest one. Sending logged-out visitors somewhere different from subscribers, or routing by user role, is a decision a static redirect rule cannot make.

Post-form redirects are another. After processing a submission you redirect so a refresh does not resubmit the form, and that has to happen in code.

And anything that depends on a value only available at runtime, like a user ID or a query parameter, belongs in PHP.

What does not belong in PHP is the ordinary case of one old URL pointing to one new URL. That is a job for a redirect manager or a server rule.

The problem that shows up later

Every method above handles one redirect well. None of them handle what happens after forty.

Change a slug and you add a hop. Change it again a year later and the old URL now points to a URL that points to a third URL. A single click travels three addresses to reach one page.

Nothing looks broken. The page still loads. But every visit is slower than it needs to be, every crawl of that path costs more than it should, and chains long enough can be abandoned before the crawler reaches the end.

Chains are also invisible in normal use, which is why they accumulate. You only find them if something is looking.

Linkilo redirect chain flattener showing detected chains with a one-click option to flatten each to a single hop
Each chain listed with every hop it passes through, and a single click to collapse it.

Linkilo’s redirect manager runs inside WordPress, detects chains, and flattens each one back to a single hop, with a 404 monitor recording which missing URLs are actually being requested so you fix the ones with traffic first.

The honest limit is that a plugin adds PHP to a request that a server rule would have handled without it. For a large migration, server-level rules are still faster, and Linkilo is WordPress only.

The short version

Use wp_safe_redirect with an explicit 301 and an exit after it. Hook it on template_redirect and keep it in a site-specific plugin.

Reach for raw header calls only outside WordPress, or when the redirect depends on logic a rule cannot express.

For a simple old-to-new mapping, do not write code at all. And whichever route you take, check for chains once you have been changing URLs for a while.

Common questions

Why is my PHP redirect showing a headers already sent error?+

Something output before the header call. Headers must go first, so a blank line before the opening PHP tag, a space after a closing tag, or an earlier echo will all break it. Check whitespace outside the PHP tags in the file and in any included files.

Do I need exit after a redirect?+

Yes, always. The header only tells the browser where to go. PHP keeps executing the rest of the script, which can send output you meant to redirect away from. This applies to wp_redirect and wp_safe_redirect too.

What is the difference between wp_redirect and wp_safe_redirect?+

wp_safe_redirect restricts the destination to your own host. Anything external is blocked unless you allow it. Use it by default, and especially when any part of the destination comes from user input, since the unsafe version can be abused to bounce visitors to another site.

Should I use PHP or .htaccess for redirects?+

Server rules for simple mappings, PHP for conditional logic. A rule in .htaccess or your Nginx config resolves without loading PHP at all, so it is faster. Write code only when the redirect depends on something known at runtime, like who is logged in.

How do I know if I have redirect chains?+

You will not notice them by browsing. Chains load normally and only show up in a crawl or a redirect report. Anything that flattens them back to a single hop will find them, and it is worth checking after any period of heavy URL changes.