Blog / Coding tips

PHP “Cannot modify header information – headers already sent”: 6 Causes and Fixes (Tested)

Cover image for PHP “Cannot modify header information – headers already sent”: 6 Causes and Fixes (Tested)

The warning Cannot modify header information - headers already sent by (output started at file.php:1) means your script sent some output to the browser before calling header(), setcookie() or session_start(). HTTP headers must come before the page body, and as soon as PHP outputs even one space, it sends the headers first. After that, they can’t be changed. The fix is to find what produced the output, which the message tells you in the output started at part, and remove it or move it after your header calls.

I reproduced each cause below with PHP 8.4.26 and its built-in web server, with output buffering turned off so the errors appear reliably, and checked the real HTTP responses with curl. File paths are shortened.

Why headers must come first

An HTTP response has two parts: the headers (status code, Location, cookies, content type) and then the body (your HTML). They’re sent in that order and only once.

The PHP manual for header() puts it plainly: “Remember that header() must be called before any actual output is sent, either by normal HTML tags, blank lines in a file, or from PHP.” Functions that need to send headers include:

  • header(), for redirects with Location: and custom headers.
  • setcookie() and setrawcookie().
  • session_start(), which sends the session cookie.
  • http_response_code().

How to read the error message

Here’s a real warning from my tests:

Warning: Cannot modify header information - headers already sent by (output started at echo.php:2) in echo.php on line 3

There are two locations in it, and the first one is the important one:

  • output started at echo.php:2 is where the stray output came from. Fix this line.
  • in echo.php on line 3 is where your header() call is. That line is fine; it’s the victim.

Beginners often stare at the header() line, which is the wrong place. Always go to the “output started at” file and line first.

Cause 1: whitespace before <?php

Anything outside <?php ... ?> is output, including a single space or blank line at the very top of a file:

 <?php
header("Location: /dashboard.php");
exit;

(Notice the space before <?php.)

HTTP/1.1 200 OK
Warning: Cannot modify header information - headers already sent by (output started at space.php:1) in space.php on line 2

The status is 200 OK instead of a redirect, because the Location header never made it out. Fix: make sure <?php is the very first thing in the file, on line 1, column 1.

Cause 2: echo, print or HTML before header()

Any deliberate output counts too:

<?php
echo "Logging in...";
header("Location: /dashboard.php");
exit;
HTTP/1.1 200 OK
Logging in...
Warning: Cannot modify header information - headers already sent by (output started at echo.php:2) in echo.php on line 3

Fix: do all your logic and header calls first, and only output HTML afterwards. A redirect page never needs a message anyway, because the browser leaves immediately. This “logic first, output last” structure is good practice in general; it’s the same pattern I use in my guide to PHP form validation and sanitisation, where the redirect after a successful submit must happen before any HTML.

Cause 3: a UTF-8 BOM at the start of the file

This one is invisible. Some editors, especially older Windows editors, save files as “UTF-8 with BOM”, adding three bytes (EF BB BF) to the very start. PHP treats them as output. I created such a file:

<?php
session_start();
echo "ok";

(Saved with a BOM before <?php.)

Warning: session_start(): Session cannot be started after headers have already been sent in bom.php on line 2

Note the different wording: session_start() has its own version of the warning. The file looks perfect in most editors. Fix: re-save the file as “UTF-8” (without BOM). In VS Code, click the encoding in the status bar and choose “Save with Encoding → UTF-8”. On Linux or macOS, head -c 3 file.php | xxd shows efbb bf if a BOM is present.

Cause 4: whitespace after a closing ?> in an included file

This is the hardest one to spot, because the output comes from a different file. Here’s an include file that ends with ?> followed by a blank line:

<?php
function helper() { return 1; }
?>

And the page that uses it:

<?php
require "inc.php";
header("Location: /dashboard.php");
exit;
HTTP/1.1 200 OK
Warning: Cannot modify header information - headers already sent by (output started at inc.php:4) in closetag.php on line 3

The message points to inc.php:4, the blank line after ?>. (PHP swallows a single newline straight after ?>, but not a second one.) Fix: remove the closing tag. The PHP manual on PHP tags recommends exactly this: “If a file ends with PHP code, it is preferable to omit the PHP closing tag at the end of the file.” With the tag removed:

<?php
function helper() { return 1; }
HTTP/1.1 302 Found
Location: /dashboard.php

The redirect works. Most coding standards, including PSR-12, require omitting the closing tag in PHP-only files for this reason.

Cause 5: warnings and notices printed to the page

If display_errors is on and an earlier line triggers a warning (for example, an undefined array key), the warning text itself is output. Then your header() call fails with a second warning. Fix: fix the first warning. In production, set display_errors=Off and log errors instead, so error messages never break headers or leak details to visitors.

Cause 6: var_dump() or print_r() left in for debugging

A forgotten var_dump($user); before a redirect does exactly the same thing as echo. Search the file named in “output started at” for debugging calls. For debugging redirects, write to the error log instead: error_log(print_r($user, true));.

Cookies fail the same way

setcookie() sends a Set-Cookie header, so it has the same rule:

<?php
echo "Welcome";
setcookie("theme", "dark");
HTTP/1.1 200 OK
Welcome
Warning: Cannot modify header information - headers already sent by (output started at cookie.php:2) in cookie.php on line 3

The response has no Set-Cookie header, so the browser never stores the cookie. Set cookies before any output, just like redirects.

Checking with headers_sent()

PHP can tell you whether headers have gone out, and from where. The headers_sent() manual page shows that its two optional arguments are filled with the file and line where output started:

<?php
echo "x";
if (headers_sent($file, $line)) {
    error_log("Headers already sent in $file on line $line");
    echo " | sent in ", basename($file), " line $line";
}
x | sent in check2.php line 2

And when nothing has been output yet, headers_sent() returns false and header() works:

<?php
if (headers_sent($file, $line)) { echo "sent at $file:$line"; exit; }
header("X-Test: 1");
echo "headers not sent yet";
HTTP/1.1 200 OK
X-Test: 1
headers not sent yet

This is useful in a helper function that redirects, so it can log a clear message instead of failing silently.

Output buffering: why it works on one server but not another

You might find that the same code works on your live host but fails locally, or the other way round. That’s usually output buffering. With buffering on, PHP holds output in memory instead of sending it immediately, so a few stray bytes don’t trigger the headers.

The php.ini-production and php.ini-development files that ship with PHP both set output_buffering = 4096, meaning PHP buffers up to 4 KB. But if PHP runs without those files, the built-in default is 0, off. I ran the same echo-then-redirect script with output_buffering=4096:

HTTP/1.1 302 Found
Location: /dashboard.php

It redirected without a warning, because “Logging in...” was still in the buffer. That hides the bug rather than fixing it: on a server with buffering off, or once a page outputs more than 4 KB, the error comes back.

Using ob_start() deliberately

You can turn buffering on in code with ob_start():

<?php
ob_start();
echo "Logging in...";
header("Location: /dashboard.php");
ob_end_clean();
exit;
HTTP/1.1 302 Found
Location: /dashboard.php

ob_end_clean() throws away the buffered text, so the user never sees it. This is legitimate for templates that build a page and might need to redirect at the end, but don’t use it as a blanket fix for messy code. Restructure the script so headers come first.

A safer redirect helper

Putting those ideas together, here’s a small helper I tested. It checks headers_sent(), logs the exact location of stray output, falls back to a link if the header can’t be sent, and always stops the script:

<?php
declare(strict_types=1);

function redirect(string $url, int $status = 302): never
{
    if (headers_sent($file, $line)) {
        error_log("redirect() failed: output started at $file:$line");
        echo '<a href="' . htmlspecialchars($url, ENT_QUOTES) . '">Continue</a>';
        exit;
    }
    header('Location: ' . $url, true, $status);
    exit;
}

Called normally, it returned a proper redirect:

HTTP/1.1 302 Found
Location: /dashboard.php

When I deliberately echoed a space first, the user got a clickable link instead of a blank page, and the error log recorded where to look:

redirect() failed: output started at redirect.php:15

The never return type (PHP 8.1+) tells static analysers that the function doesn’t return, and htmlspecialchars() keeps the fallback link safe; my guide to preventing XSS with htmlspecialchars explains why that matters.

Always exit after a Location redirect

Sending a Location header doesn’t stop your script. Without exit, PHP carries on running everything below, which can leak data or perform actions the user shouldn’t trigger. Every redirect example in this post ends with exit;. Combine that with protecting the page itself, for example with prepared statements for any database work, and your redirects stay both working and safe.

Headers already sent in WordPress themes, plugins and frameworks

The same rules apply inside any PHP application, but the “output started at” file is often one you didn’t expect:

  • A theme’s functions.php or a plugin file with a blank line after a closing ?>, or a BOM added by an editor. Because these files are loaded on every request, one stray character breaks redirects and logins site-wide.
  • A config file edited on Windows and uploaded with a BOM.
  • A custom include that echoes debugging output.

The fix is identical: open the file and line named in the warning, remove the output, and remove the closing tag if the file is pure PHP. In frameworks that build a response object (such as Laravel or Symfony), you normally return a redirect response instead of calling header() directly, and the framework sends headers at the right time. A stray echo or BOM in one of your own files can still cause the warning, though.

If your project uses Composer, keeping each class in its own PHP-only file with no closing tag is the easiest way to avoid this whole category of bug; my guide to Composer autoloading with PSR-4 shows that layout.

Quick troubleshooting checklist

  1. Read the “output started at” file and line, not the header() line.
  2. Remove anything before <?php on line 1, including spaces and a BOM.
  3. Remove closing ?> tags at the end of PHP-only files, especially includes.
  4. Move every echo, HTML block, var_dump() and print_r() below your header calls.
  5. Fix earlier warnings, and turn off display_errors in production.
  6. Don’t rely on output_buffering to hide the problem.
  7. Add exit; after every header("Location: ...").

FAQ

What does “headers already sent” mean in PHP?

It means PHP has already sent the HTTP headers because some output (text, HTML, whitespace or an error message) was sent to the browser. After that, header(), setcookie() and session_start() can’t add or change headers.

How do I find what caused headers already sent?

Look at the “output started at file.php:line” part of the warning. That file and line produced the first output. The line number after “in” is just where your header() call failed.

Why does my PHP redirect not work?

Usually because something was output before header("Location: ..."), so the header couldn’t be sent and the page returns 200 OK instead of a redirect. Fix the output, and always call exit; after the redirect.

Can a BOM cause headers already sent?

Yes. A UTF-8 byte order mark is three invisible bytes at the start of a file, and PHP outputs them. Re-save the file as UTF-8 without BOM.

Should I remove the closing ?> tag in PHP?

Yes, in files that contain only PHP. The PHP manual recommends omitting it at the end of a file, because whitespace after ?> becomes output and can cause this warning.

Does ob_start() fix headers already sent?

It stops the warning by buffering output, but it hides the underlying problem. Use it deliberately where you need it, and otherwise restructure the script so header calls come before output.

Why does the error appear locally but not on my host?

Different output_buffering settings. The php.ini files shipped with PHP set it to 4096, which hides small stray output, while PHP’s built-in default is 0. Fix the stray output and it works everywhere.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

PHP Sort Multidimensional Array by Value (usort, Tested)
Coding tips

PHP Sort Multidimensional Array by Value (usort, Tested)

usort($rows, fn($a, $b) => $a['grade'] <=> $b['grade']) sorts by one column. Descending, several columns, UK dates, case-insensitive names, keeping keys and array_multisort.

October 7, 2026 · 17 min read · 14 views

What Is msconfig? Windows System Configuration Explained for Beginners
Coding tips

What Is msconfig? Windows System Configuration Explained for Beginners

msconfig opens Windows System Configuration, a built-in troubleshooting tool for controlling how Windows starts. What each tab does, how to do a clean boot, diagnostic startup and safe boot, how to escape a safe mode loop, and why it matters for security.

October 12, 2026 · 12 min read · 0 views