PHP header('Location') redirect not working: causes, tested
header('Location: …') only works if nothing has been sent to the browser yet. We tested each common way that breaks, and why the same code can work on one server and fail on another.
1. The working version
<?php
header('Location: /dashboard.php'); // 302 Found by default
exit; // stop the script here
2. What broke it (output_buffering=0)
| File | Result |
|---|---|
A blank line before <?php | 200 OK, no redirect |
File saved as "UTF-8 with BOM" (invisible 3 bytes before <?php) | 200 OK, no redirect |
echo (or HTML) before header() | 200 OK, no redirect |
All three logged: Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/bom.php:1). The "output started at" file and line is where to look: a leading space or newline, a closing ?> followed by a newline in an included file, or a BOM (re-save the file as UTF-8 without BOM).
3. Why it works locally but not on the server (or the reverse)
With output_buffering=4096 (the value in PHP's php.ini-production), the same three files redirected fine: PHP held the stray output in a buffer, so headers could still be sent. But 5,000 bytes of output before header() overflowed the 4,096-byte buffer and the redirect failed again with the same warning. So the bug depends on the server's php.ini and on how much was printed. Fix the output, don't rely on the buffer. If you can't, call ob_start(); as the very first line.
4. Always exit after the redirect
Without exit, the response was a 302 with a Location header and the rest of the page in the body (secret page body), and the code after header() ran (it wrote a file). Browsers follow the redirect, but curl, bots or anyone reading the raw response see the page. On a login check that's a data leak.
5. 301 vs 302 and variables in the URL
header('Location: /new-page.php', true, 301); // permanent: sent "301 Moved Permanently"
$id = $_GET['id'] ?? '';
header('Location: /item.php?id=' . urlencode($id) . '&tab=1');
exit;
urlencode turned the input a&b c into a%26b+c, so it can't inject extra parameters. A lowercase location: header also worked (header names are case-insensitive). Never put raw user input into Location: only redirect to your own paths or an allow-list, or the page becomes an open redirect.
To check where many URLs end up after a site move, the tool below follows each redirect chain and reports the final status.
Frequently asked questions
Why does PHP header Location not redirect?
Something was output before header(). In our test a blank line, a UTF-8 BOM or an echo before header() gave 200 OK and 'Cannot modify header information - headers already sent'.
Why does my redirect work locally but not on the server?
output_buffering. With 4096 the stray output was buffered and the redirect worked; with 0 the same file failed. More than 4,096 bytes of output failed even with buffering.
Do I need exit after header Location?
Yes. Without it our test page still sent its full body inside the 302 response and ran the code after header().
How do I make a 301 redirect in PHP?
header('Location: /new', true, 301); followed by exit. Our test returned 301 Moved Permanently.
Written with AI assistance. Measured on 2026-10-01 with curl 8.14.1 against PHP 8.5.11 (official php:8-apache image, throwaway container) with output_buffering=0 and again with output_buffering=4096, display_errors on. The remarks about included files, ob_start() and browsers are from the PHP manual and HTTP spec, not this test. The Apify tool is ours; the APIs are free to call directly.