skip to content

Headers & Output Buffering

PHP sends headers the moment the first byte of output leaves, so header() after an echo fails unless output buffering held it back. Interviewers ask about redirects and "headers already sent".

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In PHP, what causes the "Cannot modify header information - headers already sent" warning, and how do you fix it?

level: juniorimportance: must knowfreq 72%

answer

  1. headers leave with the first body byte
  2. whitespace before <?php, or after ?>
  3. a UTF-8 BOM or a displayed warning
  4. "output started at file:line"
  5. output_buffering = 4096 can hide it

basics

~20 s

Headers are sent with the first byte of body output, so header() fails with that warning once anything has been output: an echo, whitespace outside the PHP tags, a BOM or a displayed warning. Send headers before any output.

solid answer

~50 s

HTTP puts every header before the body, so PHP sends the pending headers the moment the first byte of output leaves its buffers. After that, `header()`, `http_response_code()` and cookie functions cannot change anything: PHP emits an `E_WARNING`, "Cannot modify header information - headers already sent by (output started at /path/file.php:12)", and the script **carries on**, so a redirect silently never happens. The usual culprits are an `echo` or HTML before the call, a blank line before `<?php` or after a closing `?>` in an included file, a UTF-8 byte order mark, and a warning printed because `display_errors` is on. The message names the file and line where output began; `headers_sent($file, $line)` reports the same in code. Fix the cause: decide headers first, render afterwards, and omit the closing tag in pure-PHP files. The shipped `output_buffering = 4096` can mask the bug until early output grows past 4 KB.

code

php · 12 lines
php
<?php
declare(strict_types=1);

// Admin panel: save, then redirect - no output before this point
$saved = saveUser($_POST);            // application function

if (headers_sent($file, $line)) {
    throw new LogicException("Cannot redirect: output started at {$file}:{$line}");
}
header('Location: /admin/users', true, 303);
exit;
// no closing ?> tag in this file

go deeper

for a junior

Recall that headers must be sent before any output, and know the usual causes: echo, whitespace outside tags, a BOM and displayed warnings.

for a middle

Explain that header() only warns and the script continues, read the output-started location, and use headers_sent() to fail loudly.

for a senior

Recognise how output_buffering masks the bug, fix the ordering rather than the symptom, and keep display_errors off so warnings never become output.

for a principal

Set a structure where handlers return a response object and only one place emits headers and body, so this class of bug cannot occur.

## Why there is a deadline for headers An HTTP response is a status line, then headers, then the body. PHP collects the headers you set with `header()`, `http_response_code()` and the cookie functions in a list, and sends that list **once**, immediately before the first byte of body output reaches the server API. After that point the headers are on their way to the client and cannot be changed. "Output" means anything that reaches the output layer: `echo`, `print`, HTML outside `<?php ... ?>`, `var_dump()`, `printf()`, and PHP's own error messages when they are displayed. ## What the warning looks like and what it means Calling `header()` after output produces an `E_WARNING`: "Cannot modify header information - headers already sent by (output started at /srv/admin/src/bootstrap.php:1)" Three things to notice: - the **file and line in parentheses** are where output started, not where `header()` was called; - `header()` returns nothing and does **not** throw, so execution continues; - the header is simply dropped, so a redirect that "does nothing" is often this bug with `display_errors` off. `http_response_code()` reports the same condition with its own warning ("Cannot set response code - headers already sent") and returns `false`. ## The usual culprits | Cause | Where to look | |---|---| | `echo` or HTML before the header call | the controller or template order | | blank line or space before `<?php` | first line of the file named in the warning | | newline after a closing `?>` | the end of an included config or class file | | UTF-8 byte order mark (3 invisible bytes) | editor encoding of the named file | | a displayed notice or warning | any warning printed before the call while `display_errors` is on | | debugging output | a forgotten `var_dump()` | The line number is the fastest clue: line 1 of a file usually means a BOM or leading whitespace, and the last line means a stray newline after `?>`. ## Checking in code `headers_sent(&$filename = null, &$line = null): bool` returns `true` once headers have gone out and fills the two variables with the output start location. It is useful in a front controller to fail loudly ("cannot redirect, output started at ...") instead of silently. ## Diagnosing a redirect that silently does nothing When a save-then-redirect in an admin panel leaves the user on a blank or half-rendered page: 1. Look in the **error log** for "headers already sent"; with `display_errors` off it never appears on screen. 2. Read the **output started at** location, not the line of the `header()` call. 3. Open that file and check line 1 for a BOM or whitespace, the last line for text after `?>`, and the lines around it for `echo` or debug output. 4. If the location is a template, the handler rendered before deciding; move the redirect decision above the rendering. 5. Reproduce with `output_buffering` off, so the bug cannot hide behind a buffer. ## Fixing it properly 1. **Decide the response first, render second.** Do the work, choose the status, set headers or redirect, and only then produce HTML. 2. **Omit the closing `?>`** in files that contain only PHP; a trailing newline after it is output. 3. **Save files as UTF-8 without a BOM.** 4. **Keep `display_errors` off in production** and log instead, so a warning never becomes output. 5. **Remove debug output** before committing. ## Why output buffering hides the bug Both shipped `php.ini` files set `output_buffering = 4096`. With that, early output is held in a 4 KB buffer, and headers are not sent until the buffer is flushed. A handler that echoes a little text and then redirects "works" - until the early output grows past 4096 bytes, or the code runs under a configuration without the buffer (the compiled-in default is `0`, and the CLI forces it off). Relying on the buffer turns a clear error into an intermittent one; fix the order instead, and use explicit `ob_start()` only where capturing output is the actual goal.

  • The warning says output started at line 1 of config.php, which only defines constants; what is the likely cause?
    Something before `<?php` on line 1: a UTF-8 byte order mark saved by the editor, or a space or blank line. Both are sent as body output the moment the file is included. Re-save the file as UTF-8 without BOM and make `<?php` the very first bytes; if the warning named the file's last line, look for a newline after a closing `?>`.
  • Why does a redirect after a small echo work on one server and fail on another?
    Output buffering. With `output_buffering = 4096`, as in both shipped `php.ini` files, early output is held in a buffer and headers are still unsent when `header()` runs. On a setup with buffering off, the compiled-in default, the echo sends headers immediately and the redirect fails. The code has the bug on both; only one configuration hides it.

Headers are the envelope and output is the letter: once the first page is posted, you can no longer change the address. An output buffer is a tray on the desk that holds pages until it fills, which is why a small mistake slips through and a larger one does not.

saying these in an interview costs you the question

  • header() throws an exception when headers are already sent.
  • The warning's file and line show where header() was called.
  • Whitespace outside the PHP tags is ignored and never sent.
  • Turning on output_buffering is the proper fix.
  • A warning shown with display_errors on cannot trigger this error.
open as a page

In PHP, why must header('Location: ...') be followed by exit, and which status code does the redirect send?

level: juniorimportance: must knowfreq 66%

basics

~20 s

header('Location: ...') only queues a header; the script keeps running, so code after it still executes and its output is sent. Call exit right after. PHP adds a 302 unless a code is given, so pass 303 or 301 explicitly.

open as a page

In PHP, how do ob_start(), ob_get_clean() and ob_end_flush() work, and what does the output_buffering ini directive change?

level: middleimportance: should knowfreq 45%

basics

~20 s

ob_start() pushes a buffer that captures output; ob_get_clean() returns its contents and removes it; ob_end_flush() sends them to the next level and removes it. The output_buffering directive starts one buffer for every request: 4096 bytes in the shipped php.ini files.

open as a page

In PHP, what does header()'s $replace argument do, and how do header_remove() and http_response_code() change a pending response?

level: middleimportance: should knowfreq 40%

basics

~20 s

header() replaces an earlier header with the same name unless $replace is false, which adds another line. header_remove() deletes one header by name, or all with no argument. http_response_code() sets the status and returns the previous one.

open as a page

A PHP admin panel's CSV export of 500,000 rows exhausts memory or arrives only at the end; how do you stream it as a download?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Set the download headers, close any open output buffers, then write each row to php://output as it is fetched and call flush() periodically. Validate everything before the first row, because once output starts the status and headers can no longer change.

open as a page