Headers Already Sent Error: Causes and How to Fix It
You click "Log in" and land right back on the login screen. Or a plugin update runs, then your site throws a wall of orange text reading Warning: Cannot modify header information - headers already sent. Nothing crashed, exactly, but redirects stop working, cookies won't set, and the page above your content looks broken. Here's what's actually going on and how to fix it for good, not just hide it.
Symptom
The exact wording varies slightly, but it always looks something like this near the top of the page:
Warning: Cannot modify header information - headers already sent by (output started at /home/user/public_html/wp-content/themes/mytheme/functions.php:12) in /home/user/public_html/wp-includes/pluggable.php on line 1453
Alongside it, you'll usually see one or more of these:
- Logging into WordPress bounces you straight back to
wp-login.phpinstead of the dashboard - A "Location" redirect (a plugin sending you somewhere after saving settings) just doesn't happen
- Cookies or sessions don't persist -- you get logged out immediately, or a shopping cart empties itself
- An AJAX or REST API call returns broken JSON because the warning text got mixed into the response body
The page usually still renders. That's what makes this one sneaky -- people assume the site is "basically fine" and ignore the warning, right up until a login loop or a broken checkout makes it impossible to ignore.
Cause
PHP has one hard rule: HTTP headers -- Location: redirects, Set-Cookie:, session_start(), anything that touches the response headers -- have to be sent before any actual output reaches the browser. Not one character. The moment PHP writes a single byte of body content, the headers are locked in, and any later attempt to change them throws this warning.
The "output" that trips this is almost never something you'd expect to count as output. The usual suspects, in order of how often we actually see them:
1. A byte-order mark (BOM) at the start of a file
If a theme or plugin file was saved as "UTF-8 with BOM" instead of plain UTF-8, there are three invisible bytes sitting before the opening <?php tag. PHP treats them as output. You won't see anything in the file in a normal editor -- it just looks like an empty line.
2. Whitespace or a blank line before <?php or after ?>
A single space, tab, or blank line before the opening tag, or trailing after the closing ?> tag at the end of a file, counts as output the instant that file is included. This is extremely common in older plugins and in wp-config.php after someone edits it by hand and leaves a stray newline at the end.
3. An echo, print, or stray error before the header call
A debug var_dump() someone forgot to remove, a PHP notice or warning being displayed (see our PHP deprecated-warnings post if that's your actual problem), or a theme printing part of the HTML <head> before a plugin tries to call wp_redirect() -- all of these count.
4. Wrong include order
A plugin that calls setcookie() or header() from a hook that fires too late -- after template_redirect has already started echoing the page -- rather than from init or plugins_loaded.
How to Find the Exact Line
Ignore everything except the part of the message that says output started at. That file path and line number is not a red herring -- it is, almost always, exactly where the problem lives:
output started at /home/user/public_html/wp-content/themes/mytheme/functions.php:12
Open that file, go to that line. If line 12 is blank or you can't see anything obviously wrong, the file itself has trailing whitespace, a BOM, or something after the closing PHP tag. Check the very first and very last lines of the file first -- that's where 90% of these live.
To check for a BOM from the command line over SSH or cPanel Terminal:
head -c 3 functions.php | xxd
A BOM shows up as ef bb bf. Clean UTF-8 without one starts straight with your actual bytes (typically 3c 3f 70 for <?p).
Fix
Strip a BOM
Re-save the file as UTF-8 without BOM. Most code editors (VS Code, Sublime, Notepad++) show the encoding in the status bar and let you re-save without it. From the command line:
sed -i '1s/^\xEF\xBB\xBF//' functions.php
Remove stray whitespace around PHP tags
Delete anything before the opening <?php and, in files that only contain PHP (like wp-config.php or a plugin's main file), just delete the closing ?> tag entirely. It isn't required at the end of a file, and omitting it is the standard fix -- there's nothing after it to accidentally leave whitespace in.
Find and remove the stray output
If it's a debug statement, delete it. If it's a PHP notice being displayed rather than logged, fix display_errors in the MultiPHP INI Editor in cPanel (set it to Off on production, keep log_errors On) rather than suppressing the underlying issue.
Use output buffering as a stopgap
If you need the site working right now and can't track down the exact source immediately, wrap the entry point in an output buffer. In WordPress, this goes near the very top of wp-config.php, before the require_once line that loads wp-settings.php:
<?php
ob_start();
// ... rest of wp-config.php unchanged
This buffers output instead of sending it immediately, which gives header() and setcookie() calls a window to run even if something upstream printed a stray character. Treat this as a bandage, not a fix -- it can hide the real problem (a broken plugin still runs broken, it just doesn't throw a warning about it anymore) and adds a small amount of memory overhead on every request.
Isolate a plugin or theme conflict
If the file named in the error is inside wp-content/plugins/ or wp-content/themes/ and isn't something you wrote, check for a pending update first -- this is often already fixed upstream. If not, deactivate the plugin (via File Manager, rename its folder if you're locked out of wp-admin by the resulting redirect loop) and confirm the warning disappears before reporting it to the plugin author with the exact file and line number.
Prevention
- Never edit
wp-config.phpor plugin PHP files in a plain-text or Windows-default editor that might silently add a BOM -- use an editor that shows and lets you control the encoding - Drop the closing
?>tag from any file that's pure PHP -- there's no downside and it removes an entire class of this bug - Keep
display_errorsoff in production; log to a file instead so notices don't leak into the response body - When writing your own plugin code, call
header(),setcookie(), orwp_redirect()from an early hook likeinit, not from template output
Symptom-to-Cause Quick Reference
| What you see | Likely cause | Where to look |
|---|---|---|
| Warning on nearly every page, line number points to a core WordPress file | Output started in a theme or plugin file, not WordPress core itself | The "output started at" path in the warning -- ignore the core file, check that one |
| Login redirects back to wp-login.php in a loop | A cookie/session couldn't be set because output already started | functions.php, wp-config.php, or a security/caching plugin's early hooks |
| Warning only after a plugin update | New plugin version added a stray echo, debug call, or BOM | The plugin's main PHP file -- check the changelog or roll back a version |
| Broken JSON from an AJAX or REST call | A notice/warning got mixed into the response body | PHP error log, MultiPHP INI Editor display_errors setting |
| Warning references wp-config.php specifically | Blank line or whitespace before/after the PHP tags in wp-config.php | The very first and very last lines of the file |
Frequently asked questions
Does this error actually break my site, or is it just a warning?
It's a warning, so PHP execution continues and the page still loads. But anything that depends on setting a header after that point -- redirects, cookies, sessions -- silently fails, which is what causes login loops and broken checkouts even though the page itself renders.
I fixed the file but the warning still shows up on every page. Why?
Check for output caching -- a page cache plugin, OPcache, or a CDN may be serving a cached copy of the broken response. Purge the cache after your fix and reload with a hard refresh before assuming the fix didn't work.
Can I just suppress the warning with error_reporting instead of fixing it?
You can hide the text, but the underlying header() or setcookie() call still silently fails -- you'll just lose the warning that told you why logins or redirects aren't working. Fix the source instead; use ob_start() as a temporary buffer only if you need the site usable while you track it down.
How do I know if it's a theme problem or a plugin problem?
The file path in "output started at" tells you directly -- if it's under wp-content/themes/yourtheme/, it's the theme; under wp-content/plugins/somefolder/, it's that plugin. Deactivate just that one item and confirm the warning disappears before making further changes.
Is it safe to just delete the closing ?> tag from my files?
Yes, for any file that contains only PHP code. The closing tag is optional in PHP, and removing it is a standard, safe practice recommended for exactly this reason -- it eliminates trailing whitespace after it as a possible cause entirely.