A headers already sent warning traced to session_start at file scope in a child theme, fixed at the cause instead of buried under an output buffer
A line of warning text across the top of every page. Admin redirects that do not complete. A login that works but lands you somewhere unexpected. Underneath all of it, the same message:
Warning: Cannot modify header information, headers already sent by (output started at /path/to/file.php:14)
Search that message and you will get the same answer almost everywhere: wrap it in an output buffer. Add ob_start() and the warning disappears.
It does disappear. That is the problem.
Every HTTP response has two parts, and they go out in a fixed order. Headers first, which carry things like the status code, cookies and redirects. Then the body, which is the HTML.
Once a single byte of body has left the server, the headers are already gone. They cannot be amended, because they have been sent. That is not a WordPress rule, it is how HTTP works.
So anything that needs to set a header has to run before any output happens. Redirects, cookies, sessions, cache headers, security headers. When something prints first, every one of those silently fails for the rest of the request.
The warning is not the bug. The warning is WordPress telling you that a bug already happened, slightly earlier, somewhere else.
The useful half is in the parentheses: output started at /path/to/file.php:14.
That is not where the error happened. That is the file and line number of the first thing that printed. It is the cause, handed to you, and it is routinely ignored in favour of pasting the first half of the message into a search box.
Open that file, go to that line, and the answer is almost always one of a small set of things.
?> at the end of a file is output. This is why experienced developers leave the closing tag off PHP-only files entirely. Nothing can trail it if it is not there.echo, a leftover debug line, a warning from unrelated code that is being displayed rather than logged.On this site the trigger was session_start() sitting at the top level of a child theme’s functions file.
Two things make that fragile. The first is that session_start() sends a cookie, so it is a header operation, which means it inherits every restriction above. The second is timing. Code at file scope runs the moment the file is loaded, and a theme’s functions file loads after the entire plugin stack. If anything in that stack has produced even a newline, the session call is already too late.
It also means the failure is environment-dependent. The same theme behaves perfectly on one site and warns on another, because the difference is what else is installed. That is why this gets misdiagnosed as a theme bug, and why it often appears right after an unrelated plugin is activated.
Child themes add one more wrinkle: the child’s functions file loads before the parent’s. Code moved into a child theme during a customisation can end up running at a different point than it did before, and start failing without anyone having touched it.
An output buffer catches everything the page prints and holds it in memory instead of sending it. Headers stay unsent, so they stay modifiable, and the warning stops.
Nothing has been fixed. The stray output is still there, still being generated, and now you cannot see it. The next person to hit a header problem on this site has lost the one diagnostic that would have told them where to look.
It also has costs. The page no longer streams, so the browser receives nothing until the whole response is built, which works against the time-to-first-byte you are otherwise trying to protect. The buffer holds the entire page in memory. And a buffer opened and never closed cleanly can swallow genuine errors, turning a loud problem into a silent one.
There is a narrow legitimate use for output buffering. Papering over accidental output is not it.
Find the real source first, using the file and line the warning already gave you. Fix that. In most cases it is deleting a closing tag or re-saving a file, and the warning goes away because the cause is gone.
If the code genuinely needs a session, do not call it at file scope. Hook it to init, which runs early enough to be before output but late enough that WordPress is loaded. Check whether a session already exists before starting another.
Then ask the harder question: does this need a PHP session at all?
PHP sessions and full-page caching do not coexist well.
A session issues a per-visitor cookie. Caching layers treat a cookie as a signal that the response is personal and must not be shared, so a session started on every request can take a site that was serving cached pages and quietly drop it back to generating every page from scratch.
The site still works. It just gets slower, for a reason that will not appear in any plugin’s settings screen, and that nobody will connect to a warning message fixed months earlier.
Most things people reach for sessions to do in WordPress have a better home: transients for short-lived server-side state, user meta for anything belonging to a logged-in account, a plain cookie for a preference that does not need protecting. Those cost nothing in cache hit rate.
?>. Then remove the closing tag rather than the whitespace.session_start at file scope. If it is there, move it onto init or remove the need for it.ob_start() in as the fix. If it is already there, take it out and see what the site tells you.Warning gone because the output causing it was removed, session handling moved onto a proper hook, and no output buffer left behind hiding the next one.
The general version: “headers already sent” is one of the few WordPress errors that tells you exactly where to look, in the part of the message most people scroll past. The popular fix suppresses the evidence and keeps the cause, which is how the same problem comes back later wearing a performance costume instead.
If you have a warning you have been living with, or a site that got slower after someone made one go away, that is bug fixing and speed work in the same visit. If it is on a Divi build or a child theme somebody else wrote, that is the usual place it hides.
Free 30-minute call. Tell me what is going wrong and you get a fixed-price quote within 48 hours. If a smaller fix would do, I will say so.