skip to content

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%

answer

  1. a stack of buffers, ob_get_level()
  2. get_clean returns and discards
  3. end_flush passes to the level below
  4. output_buffering: 0 compiled, 4096 in php.ini
  5. On means an unlimited buffer

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.

solid answer

~40 s

Output buffers form a **stack**: `ob_start()` pushes one, and everything echoed lands in the top buffer instead of going out. `ob_get_clean()` returns the top buffer's contents as a string and discards the buffer (`false` if there is none); `ob_end_flush()` passes the contents down to the next level, or to the client, and removes the buffer; `ob_get_contents()` peeks, `ob_get_level()` reports the depth. `ob_start($callback, $chunk_size)` can filter the output or flush automatically when it reaches `$chunk_size`. The `output_buffering` directive opens one buffer before your script runs: compiled default `0` (off), `4096` in both shipped `php.ini` files, and `On` means unlimited. It is `INI_PERDIR` and forced off in the CLI. While a buffer holds output, headers remain unsent, which is why buffering can hide header-order bugs. PHP 8.5 deprecates producing output from inside a user output handler.

code

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

function renderToString(string $template, array $vars): string
{
    $level = ob_get_level();
    ob_start();
    try {
        require $template;              // the template reads $vars
        return (string) ob_get_clean();
    } finally {
        while (ob_get_level() > $level) { // only runs if require threw
            ob_end_clean();
        }
    }
}

$body = renderToString(__DIR__ . '/mail/user-created.php', ['name' => 'Ana']);

go deeper

for a junior

Recall that ob_start() captures output and ob_get_clean() returns it as a string, which is how you render a template into a variable.

for a middle

Explain the buffer stack, the difference between the get, end and flush functions, and what output_buffering values mean.

for a senior

Always close buffers in finally, account for the automatic level-1 buffer, and avoid unlimited buffering on large responses.

for a principal

Decide whether the application renders into buffers at all or builds responses as strings or streams, and set output_buffering to match.

## The buffer stack PHP routes all output - `echo`, `print`, inline HTML, `printf()` - through an output layer. By default output goes straight to the server API. An **output buffer** intercepts it and keeps it in memory. Buffers nest: - `ob_start()` pushes a new buffer; output now lands in it; - `ob_get_level()` returns how many buffers are active; - closing a buffer hands its contents to the buffer below, or to the client if it was the last one; - at the end of the request PHP flushes any buffers still open. ## The functions that close or read a buffer | Function | Returns | Contents go | Buffer afterwards | |---|---|---|---| | `ob_get_contents()` | `string\|false` | stay in the buffer | still active | | `ob_get_clean()` | `string\|false` | to your variable | removed | | `ob_end_flush()` | `bool` | to the level below | removed | | `ob_end_clean()` | `bool` | discarded | removed | | `ob_flush()` | `bool` | to the level below | still active, emptied | | `ob_clean()` | `bool` | discarded | still active, emptied | With no active buffer, `ob_get_contents()` and `ob_get_clean()` just return `false`, while the flush, clean and end functions return `false` and emit an `E_NOTICE` such as "Failed to delete buffer. No buffer to delete". ## `ob_start()` options `ob_start($callback = null, int $chunk_size = 0, int $flags = PHP_OUTPUT_HANDLER_STDFLAGS): bool` - **`$callback`** - an output handler that receives the buffered string and returns what should be passed on; it can rewrite or compress output. Since PHP 8.5, echoing from **inside** a user handler is deprecated. - **`$chunk_size`** - with `0` (the default) everything is held until the buffer is closed; with a positive value the buffer flushes whenever its length reaches that size, which caps its memory. - **`$flags`** - whether the buffer may be cleaned, flushed or removed by the functions above. ## The `output_buffering` directive `output_buffering` makes PHP call the equivalent of `ob_start()` before the script starts: | Value | Meaning | |---|---| | `0` / `Off` | no automatic buffer (compiled-in default) | | `4096` | a buffer that flushes every 4096 bytes (both shipped `php.ini` files) | | `On` / `1` | a buffer with no size limit, held until the end of the request | It is `INI_PERDIR`, so it can be set in `php.ini`, `.htaccess`, `.user.ini` or a PHP-FPM pool, but not with `ini_set()`, and the CLI forces it off. The automatic buffer shows up in `ob_get_level()` as level 1, which matters for code that "closes all buffers". ## Buffers and headers Headers are sent when output first leaves the **outermost** buffer. While a buffer holds output, `header()` still works. That is useful - render a template into a buffer, then decide on a status or redirect - and dangerous, because the 4096-byte default quietly tolerates early output until a page grows. ## Common mistakes - **Unbalanced buffers.** An `ob_start()` whose matching close is skipped by an exception leaves a buffer open; its contents are flushed at the end of the request, often after an error page. - **Closing someone else's buffer.** A library that calls `ob_end_clean()` in a loop until `ob_get_level()` is 0 also removes the automatic buffer and any buffer the framework opened. - **Using buffering to fix header order.** It hides the bug only while early output stays under the buffer size. - **Unlimited buffers on large responses.** With `output_buffering = On` or `ob_start()` without a chunk size, the whole response sits in memory and counts against `memory_limit`. - **Expecting `flush()` to empty user buffers.** It does not; it works on the layer below them. ## Typical uses 1. **Render to a string.** Capture a template for an e-mail body or a cached fragment with `ob_start()` ... `ob_get_clean()`. 2. **Discard stray output** before sending a file download, by closing every open buffer. 3. **Decide late.** Render first, and if something fails, discard the partial HTML and send an error page with the right status instead. Always pair `ob_start()` with a close in a `finally` block, so an exception cannot leave a buffer open and send half a page later.

  • Why does ob_get_level() often return 1 before your code has called ob_start()?
    With `output_buffering` set to a non-zero value, as in both shipped `php.ini` files (`4096`), PHP opens a buffer before the script starts, and it counts as level 1. Code that closes "all" buffers with a `while (ob_get_level() > 0)` loop also closes this one, which is intended before a raw file download but surprising elsewhere.
  • What is the difference between output_buffering = On and output_buffering = 4096?
    `On` (1) creates a buffer with no size limit: the whole page stays in memory until the request ends, and headers stay unsent. `4096` creates a buffer that flushes whenever it reaches 4096 bytes, so memory stays bounded and output streams in chunks. For large responses `On` can push a script into `memory_limit`.

saying these in an interview costs you the question

  • ob_get_clean() sends the buffer to the browser.
  • Output buffering is off by default in the shipped php.ini files.
  • ini_set('output_buffering', '0') turns it off for the current request.
  • ob_end_flush() leaves the buffer active after sending it.
  • Only one output buffer can be active at a time.