Fixing the Headers Already Sent Warning
The warning names the file and line that produced output too early, in the phrase in parentheses. Everything else in the message is the victim. Fix the file it names, and the login redirects, feeds and cookie errors that came with it stop together.
The message has a cause and a victim, and most people fix the victim. "Warning: Cannot modify header information - headers already sent by (output started at /home/site/wp-content/plugins/example/example.php:12) in /home/site/wp-includes/pluggable.php on line 1435" says that pluggable.php tried to send an HTTP header and could not, because example.php had already printed something on line 12. The file in the parentheses is the one to open. The file at the end is where WordPress noticed.
Why output breaks headers
HTTP headers — redirects, cookies, content type — must be sent before any body content. The moment PHP prints a single byte, the headers are committed and no more can be added. WordPress sends headers late, after loading plugins and the theme, so anything those files print on load fires first. On an ordinary page the stray byte lands at the top of the HTML and nobody notices. On a request that must redirect or set a cookie — login, logout, a form submission, a feed with its content type — the header fails and the warning appears.
Find the output
The parentheses give the file and line. The line is where output started, and the common contents of that line are:
| What is on the line | Cause |
|---|---|
Line 1, and it is <?php |
Something before it: a BOM or a blank line |
The last line, after ?> |
A blank line or a space after the closing tag — PHP swallows exactly one newline there, nothing more |
An echo, print, var_dump or HTML outside <?php tags |
Debug output or template code running on load |
| A PHP warning or notice | PHP printed an error message because display_errors is on |
wp-config.php:1 or its last line |
The config file has whitespace or a BOM |
The BOM case is the invisible one. An editor that saves UTF-8 with a byte order mark puts three bytes before <?php that most editors do not display. Check the first bytes:
head -c 3 wp-config.php | xxd
efbb bf is the BOM. Re-save the file as UTF-8 without BOM, or strip it:
sed -i '1s/^\xEF\xBB\xBF//' wp-config.php # GNU sed; on macOS use sed -i ''
Trailing whitespace after ?> is the second invisible one. The fix is to remove the closing tag entirely — PHP does not require it, and core, themes and plugins written in the last decade omit it for exactly this reason. A file that ends in ?> followed by a blank line or trailing spaces is a file waiting to cause this warning, and functions.php edited by hand is the usual place to find one. Whether a stray byte breaks anything also depends on output_buffering: the production php.ini ships with a 4 KB buffer that absorbs small output, which is why the same file is harmless on one host and breaks login on another.
When the line is a PHP notice
If the output that started things was a warning from PHP itself — a deprecated call, an undefined variable in a plugin — then the real bug is that warning, and the header problem is a side effect of display_errors being on. Production sites should not display errors: set WP_DEBUG_DISPLAY to false and WP_DEBUG_LOG to true so the notice goes to the log rather than the response. Enabling debug mode covers the three constants; the notice itself still needs fixing, but it stops breaking logins while you do.
Symptoms that trace back to this
- Login succeeds but stays on the login page, or shows the warning above the form. The redirect header was refused. Often filed as wp-admin not working.
- RSS feed shows an XML parse error because of a character before the
<?xmldeclaration — the stray output. - "Cookies are blocked due to unexpected output" on login — WordPress prints that specific variant when it detects headers were already sent.
- Downloads or exports produce a corrupted file, with the stray output prepended.
- Plugins that redirect after saving settings return a blank page with the warning.
Any one of these on a site that was recently edited by hand points here before anything else.
Finding it without the warning
If display_errors is off and the symptom is a broken redirect with no message, the debug log has the warning. If even that is empty, request a page over the command line and inspect the first bytes of the response for anything before <!DOCTYPE:
curl -s https://example.com/ | head -c 20 | xxd
A space, newline or BOM before the doctype is the early output, and the file is one of the recently changed ones: git status on a versioned site, or the modification times under wp-content otherwise. Disabling plugins one at a time, as in plugin conflict testing, isolates it when the log is not available.
Verify
Log out and log in again: you land on the dashboard with no warning. Load the RSS feed: the first bytes are <?xml. Check the response of the home page with the curl command above: it begins with <!DOCTYPE. Finally, confirm WP_DEBUG_DISPLAY is false on production so a future notice from a plugin update does not reintroduce the problem through the display path.
Frequently asked
- No. pluggable.php is where WordPress sends the redirect or cookie header that failed. The cause is in the parentheses: output started at /path/file.php:line. That file printed something before headers were sent, and it is almost always a plugin, a theme, or wp-config.php.
- Whitespace. A blank line before the opening <?php tag, a blank line or trailing space after a closing ?> at the end of the file (PHP forgives exactly one newline there), or a UTF-8 byte order mark your editor hides. Open the file in an editor that shows invisible characters, or check with a hex viewer: the first bytes must be <?php.
- Normal pages tolerate early output; it just appears at the top of the HTML. Login, logout and redirects need to send headers after processing, and the early output has already sent them. The login succeeds and the redirect fails, leaving you on wp-login.php.