Output Buffering
What Is Output Buffering? In simple terms: Normally, everything a PHP script prints with echo , print , or HTML outside <?php ?> is sent toward the browser right away. Output buffering tells PHP
What Is Output Buffering?
In simple terms: Normally, everything a PHP script prints with echo, print, or HTML outside <?php ?> is sent toward the browser right away. Output buffering tells PHP to collect the output in memory first. You can then send it later, change it, store it in a variable, or throw it away.
Technically: The output control functions (ob_*) manage a stack of output buffers. ob_start() pushes a new buffer; output then goes into the topmost buffer instead of the next layer. Functions such as ob_get_clean() retrieve and remove a buffer, and ob_end_flush() passes its content to the next layer (or to the client). A buffer can also have a callback that transforms its content.
Why It Is Useful
| Use | Example |
|---|---|
| Capturing output as a string | Rendering a template into a variable for a layout or an email |
| Sending headers after output has started | A redirect decided after part of a page was generated |
| Transforming output | Replacing placeholders, minifying HTML |
| Discarding output | Ignoring output from code you cannot change |
| Compressing output | Gzip compression with ob_gzhandler |
Capturing Output
Example
<?php
ob_start();
echo "Hello";
print " from PHP";
?>
and from HTML
<?php
$output = ob_get_clean();
echo strtoupper(trim($output)) . "\n";
Output
HELLO FROM PHP AND FROM HTML
How It Works
ob_start()begins buffering.echo,print, and the HTML between the PHP blocks all go into the buffer — nothing is sent. (The newline directly after?>is removed by PHP, so the buffer containsHello from PHP and from HTMLfollowed by a newline.)ob_get_clean()returns the buffered text and removes the buffer.- The captured text is now an ordinary string that can be modified before printing.
Core Functions
| Function | What It Does |
|---|---|
ob_start($callback = null) | Start a new buffer |
ob_get_contents() | Return the current buffer's content (buffer stays active) |
ob_get_clean() | Return the content and remove the buffer |
ob_end_clean() | Discard the content and remove the buffer |
ob_end_flush() | Send the content to the next layer and remove the buffer |
ob_flush() / ob_clean() | Send / discard the content but keep the buffer active |
ob_get_level() | Number of active buffers |
ob_get_length() | Size of the current buffer in bytes |
Nested Buffers
Buffers form a stack. Output always goes to the topmost buffer:
<?php
echo ob_get_level() . "\n"; // command line: 0
ob_start();
echo "outer ";
ob_start();
echo "inner";
$inner = ob_get_clean(); // closes the inner buffer
echo "[" . strtoupper($inner) . "]";
$outer = ob_get_clean(); // closes the outer buffer
echo $outer . "\n";
Output
0
outer [INNER]
The inner buffer's content was captured separately and then added to the outer buffer in modified form. This is how template engines render a page inside a layout: the page is captured first, then inserted into the layout.
Note: On web servers,
ob_get_level()often starts at1, because theoutput_bufferingsetting inphp.ini(commonly4096bytes) creates a buffer automatically. In the command line, this setting is off, so the level starts at0. Code should not assume a specific starting level.
Transforming Output With a Callback
A callback receives the buffered content and returns the version that is actually sent:
<?php
ob_start(fn(string $buffer): string => str_replace('{{year}}', '2026', $buffer));
echo "© {{year}} GoCourse\n";
ob_end_flush();
Output
© 2026 GoCourse
Example: Removing Whitespace Between HTML Tags
<?php
ob_start(fn(string $html): string => preg_replace('/>\s+</', '><', $html));
?>
<ul>
<li>Pen</li>
<li>Notebook</li>
</ul>
<?php
ob_end_flush();
Output
<ul><li>Pen</li><li>Notebook</li></ul>
Whitespace between a closing > and the next < is removed, reducing the page size. (Real minification must preserve whitespace inside <pre>, <textarea>, and inline text; dedicated libraries or server-side compression are safer choices.)
Compression
ob_start('ob_gzhandler');
ob_gzhandler compresses the output with gzip when the browser supports it. In most setups, compression is better done by the web server (Nginx gzip, Apache mod_deflate), which also handles static files.
Headers After Output
Headers must be sent before the body. While output is still buffered, nothing has been sent yet, so headers can still be added:
<?php
ob_start();
echo "<p>Processing your order...</p>";
$paymentFailed = true;
if ($paymentFailed) {
ob_end_clean(); // discard the partial page
header('Location: /checkout.php?error=payment', true, 303);
exit;
}
ob_end_flush();
The partial output is discarded and a redirect is sent instead. Relying on this is a workaround; a cleaner design decides on redirects before generating any output. It is, however, useful when integrating code that prints output in unexpected places.
Exception-Safe Capturing
If code inside a buffer throws an exception, the buffer stays open unless it is closed explicitly. A helper with finally guarantees that buffers are always cleaned up:
<?php
function capture(callable $callback): string
{
ob_start();
try {
$callback();
return ob_get_contents();
} finally {
ob_end_clean();
}
}
echo capture(fn() => print("Hi there")) . "\n";
$levelBefore = ob_get_level();
try {
capture(function (): void {
echo "partial output";
throw new RuntimeException('Template error');
});
} catch (RuntimeException $e) {
echo "Caught: " . $e->getMessage() . "\n";
}
var_dump(ob_get_level() === $levelBefore);
Output
Hi there
Caught: Template error
bool(true)
How It Works
ob_get_contents()reads the buffer beforefinallyremoves it withob_end_clean().- If the callback throws, the
finallyblock still discards the partial output and closes the buffer, so the buffer level is the same as before. - Without this, the partial output ("partial output") could later appear in the middle of an error page.
Practical Example: A Page With a Layout
<?php
function render(string $template, array $data): string
{
return capture(function () use ($template, $data): void {
extract($data, EXTR_SKIP);
include $template;
});
}
function capture(callable $callback): string
{
ob_start();
try {
$callback();
return ob_get_contents();
} finally {
ob_end_clean();
}
}
// templates/page.php
file_put_contents(__DIR__ . '/page.php', '<h1><?= htmlspecialchars($title) ?></h1>' . "\n");
// templates/layout.php
file_put_contents(__DIR__ . '/layout.php', "<html><body>\n<?= \$content ?></body></html>\n");
$content = render(__DIR__ . '/page.php', ['title' => 'PHP Courses']);
echo render(__DIR__ . '/layout.php', ['content' => $content]);
Output
<html><body>
<h1>PHP Courses</h1>
</body></html>
Step-by-Step Explanation
- The page template is rendered into a string using output buffering.
- That string is passed to the layout template as
$content. - The layout is rendered the same way and printed.
$contentis printed in the layout withouthtmlspecialchars()because it is already-escaped HTML produced by our own template; all user data was escaped in the page template.
This two-step rendering is the basis of layouts in template engines such as Blade and Twig.
Streaming Output: flush()
Sometimes you want output sent immediately, for example to show progress during a long task:
<?php
while (ob_get_level() > 0) {
ob_end_flush();
}
foreach (['Importing products', 'Updating prices', 'Clearing cache'] as $step) {
echo $step . "...\n";
flush();
sleep(1); // simulated work
}
Closing all buffers and calling flush() pushes output toward the client. Whether the user sees it immediately also depends on the web server and proxies, which may buffer responses themselves (for example, Nginx's fastcgi_buffering). For real-time progress in browsers, Server-Sent Events or background jobs with status polling are more reliable.
Common Mistakes
Leaving Buffers Open After Exceptions
Effect: Partial output appears in unexpected places.
Fix: Close buffers in finally.
Assuming the Initial Buffer Level Is 0
Effect: Code that closes "all" buffers breaks other layers (such as framework buffers).
Fix: Record the level before starting and restore exactly that level.
Using Buffering to Hide Design Problems
Effect: Headers are sent late "because buffering makes it work", hiding a confusing flow.
Fix: Decide on headers and redirects before rendering output.
Buffering Huge Output
Effect: Large exports are held entirely in memory.
Fix: Stream large output directly (php://output, fputcsv()), without buffering.
Best Practices
- Use output buffering mainly to capture template output as strings.
- Always pair
ob_start()with a closing call, ideally infinally. - Prefer server-level compression over
ob_gzhandler. - Do not rely on buffering to send headers late; structure code so headers come first.
- Stream large or long-running output instead of buffering it.
Official reference: https://www.php.net/manual/en/book.outcontrol.php