Output Buffering

JA JASWANT Updated 17 Oct 2026
6 min read ·Lesson 104 of 248
In this post

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

UseExample
Capturing output as a stringRendering a template into a variable for a layout or an email
Sending headers after output has startedA redirect decided after part of a page was generated
Transforming outputReplacing placeholders, minifying HTML
Discarding outputIgnoring output from code you cannot change
Compressing outputGzip 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

  1. ob_start() begins buffering.
  2. 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 contains Hello from PHP and from HTML followed by a newline.)
  3. ob_get_clean() returns the buffered text and removes the buffer.
  4. The captured text is now an ordinary string that can be modified before printing.

Core Functions

FunctionWhat 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 at 1, because the output_buffering setting in php.ini (commonly 4096 bytes) creates a buffer automatically. In the command line, this setting is off, so the level starts at 0. 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

  1. ob_get_contents() reads the buffer before finally removes it with ob_end_clean().
  2. If the callback throws, the finally block still discards the partial output and closes the buffer, so the buffer level is the same as before.
  3. 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

  1. The page template is rendered into a string using output buffering.
  2. That string is passed to the layout template as $content.
  3. The layout is rendered the same way and printed.
  4. $content is printed in the layout without htmlspecialchars() 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 in finally.
  • 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

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else User-defined Functions Slicing Strings Function Arguments PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning MySQL Database MySQL Connect MySQL Create DB MySQL Create Table MySQL Insert Data MySQL Get Last ID MySQL Insert Multiple MySQL Prepared MySQL Select Data MySQL Where Debugging PHP Project: Image Upload Gallery $_SESSION $_ENV PHP RegEx RegEx Patterns Heredoc and Nowdoc RegEx Modifiers preg_match preg_replace preg_split PHP RegEx Functions Nullable Types Mixed and Never Types Nullsafe Operator PHP Attributes PHP Fibers Readonly Classes Property Hooks (PHP 8.4) Asymmetric Visibility (PHP 8.4) Reflection API SPL Data Structures Composer PHP Form Handling PDO Transactions PDO Error Handling PHP XML Parsers PHP SimpleXML Parser PHP SimpleXML - Get PHP XML Expat Parser PHP DOM Parser PHP XMLReader PHP XMLWriter AJAX Intro PHP Class Constants Encryption with Sodium Recursive Functions Variable Functions Anonymous Functions Closures Arrow Functions First-class Callable Syntax PHP Arrays Indexed Arrays Associative Arrays Create Arrays Variables Scope Static Variables Variable Variables PHP Echo / Print PHP printf / sprintf PHP Data Types PHP Booleans PHP NULL PHP Strings String Functions array_map / array_filter / array_reduce Array Functions PHP Superglobals $GLOBALS What Is `$_SERVER` ? $_REQUEST $_POST $_GET $_FILES $_COOKIE PHP Type Juggling MySQL Order By MySQL Delete Data MySQL Update Data MySQL Limit Data MySQL Joins with PHP Pagination with PHP PDO Introduction PDO Connect PDO Prepared Statements PDO Fetch Modes PSR-4 Autoloading Installing Extensions (PECL / PIE) OPcache & Performance PHP-FPM Project: Contact Form with Email Project: Login & Registration System Project: CRUD App with MySQL Project: Shopping Cart with Sessions Project: REST API with PHP DateTime Class Timezones PHP Include Include vs Require PHP File Handling PHP File Open/Read PHP File Create/Write File Delete / Rename / Copy PHP Directories PHP CSV Files
Ask about this post
AI Ask about this post

Ask questions about Output Buffering and get answers drawn from it.

Signed-in readers only.