PHP output buffering

Did you ever wonder why does your flush() call not work? Fortunately there're plenty of possible explanations. Smile You might have only one issue preventing you from a successful flush(), but chances are high that you'll face several problems at the same time.

The buffering of the output of your PHP script might occur at three places:
  1. on the webserver
  2. between the webserver and the browser
  3. in the browser
One might split down the second part even more (eg. by taking every service other than the webserver on the application server as a separate category, and taking every application other than the browser on the client as a separate category), but usually these three categories are sufficient. Let's now assume that the webserver is directly communicating with the browser, without any extra layers (eg. server side proxy, client side proxy, or some encrypted channel) between the two.

On the server side you should deal with two components: the webserver (Apache, Lighttpd, etc.) and the PHP extensions. The Apache webserver can load dozens of modules and a lot of these can or do apply output buffering, which means your ob_end_flush() and flush() calls have not much effect. According to my experience, you're better off using PHP as a loadable module, because in this case flush() works even with output buffering (but you should test for yourself just to go for sure). In PHP CGI mode (or fast CGI) your flush() calls have no effect at all on how Apache (and its modules) buffer your output. So to make flush() work, you'll have to disable output buffering in all components of the webserver. Eg. do not use mod_deflate (or mod_gzip in earlier versions of Apache) to gzip compress your output, because it does use buffering. There's no comprehensive list of buffering modules (as far as I know) and since new modules are created all the time, such a list would be falling always behind the actual status. The only way to go is to see for yourself. Try to disable as many Apache modules as possible and test output buffering. If it's not yet present, then re-enable a couple of modules and test again. Do this until you find all modules that buffer the output. You'll find a handy little script at the end of this article that might be of help in the process of finding the output buffering modules.

Of course not only Apache, but the PHP interpreter itself might be buffering the output. You can disable PHP's output buffering by setting the output_buffering INI directive to zero in your configuration file (or runtime via the ini_set() function call). PHP's zlib extension might be buffering too, so you should also disable output compression using the zlib.output_compression directive. You can safely do so with the following code snippet:
<?php
if (function_exists('gzencode')) {
 
$zlib_compression strtolower(ini_get('zlib.output_compression'));
  if (
$zlib_compression != '' && $zlib_compression != 'off' && $zlib_compression != '0') {
   
ini_set('zlib.output_compression''Off');
  }
}
?>

There might be other PHP extensions buffering the output too, so go through them one by one.

On the client side the major problem is the browser itself. Mozilla Firefox used to handle output flushing quite well, but it is possible that some HTML structures are cached to make user experience more pleasant. Eg. some versions of Internet Explorer (IE) do buffer HTML tables, so do not expect any visual output if you're flushing while inside an HTML table. Another IE weirdness is that some versions do not output anything until a specific number of bytes are not received (~256). You can work around this issue either by putting bogus header lines into the HTTP response or by putting bogus (and invisible) characters into the response body (eg. whitespace is usually collapsed in HTML pages, so you can use space characters to make the output longer without any sideeffects).

One important thing about IE7 (and possibly other versions of IE too) is that it buffers output if no "Content-Length" header is present among the reponse HTTP headers. At least my attempts to redirect the browser to some other page using a "Location:" header failed, unless I added a proper "Content-Length" header. You might add a "Connection: close" header too (HTTP/1.1 clients), but that doesn't seem very important.

To summarize my thoughts: flushing in PHP scripts is a very fragile thing since output buffering can occur at so many levels that the developer cannot affect. You should try to avoid the need of a flush(), but if you cannot, the above tips might give you a good starting point for your debugging.

And as a closure, here's a PHP script that you can use to determine whether the output of your PHP script is buffered or not:
<?php
  $latency 
5;
 
$ptime NULL;
 
$time time();
  if (
array_key_exists('time'$_GET)) {
   
$ptime intval($_GET['time']);
  }

 
$url 'http://' $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'];

  if (
$ptime 0) {
    print 
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD>
<TITLE>Test page</TITLE>
</HEAD>
<BODY><P>
started: ' 
date('Y.m.d h:i:s'$ptime) . '<BR/>
finished: ' 
date('Y.m.d h:i:s'$time) . '<BR/>
Note: if flush() works in PHP, then the difference between start and end dates
 should be approx. 0-1 second. If flush() fails (PHP output is buffered), then the
 difference will be >= ' 
$latency ' seconds.
<BR/><BR/>
'
;
    if (
abs($ptime $time) >= $latency) {
      print 
'<SPAN STYLE="color: red">Flush failed. :-(</SPAN>';
    }
    else {
      print 
'<SPAN STYLE="color: green">Flush succeeded! :-)</SPAN>';
    }
    print 
'<BR/>
<FORM>
<INPUT TYPE="button" ONCLICK="document.location=\'' 
$url '\';" VALUE="Retry">
</FORM>
</P></BODY>
</HTML>
'
;
  }
  else {
   
// we detect the presence of the zlib extension by looking for the existence of the gzencode function
   
if (function_exists('gzencode')) {
     
$zlib_compression strtolower(ini_get('zlib.output_compression'));
      if (
$zlib_compression != '' && $zlib_compression != 'off' && $zlib_compression != '0') {
       
ini_set('zlib.output_compression''Off');
      }
    }

   
ob_start();

    print 
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD>
<TITLE>Test page</TITLE>
<SCRIPT TYPE="text/javascript">
<!--
  document.location="' 
$url '?time=' $time '";
//-->
</SCRIPT>
</HEAD>
<BODY><P></P></BODY>
</HTML>
'
;
   
header('Connection: close');
   
header('Content-Length: ' ob_get_length());

   
// Note: the "Content-Length" header with the proper length is absolutely required by IE.
    // Firefox does render/interpret flushed server output even without it, but IE wont.
    // If the connection is not closed by the server, but the output is flushed to the browser,
    // and there is no Content-Length, then IE will simply wait till output is finished ...
    // even if it got a "Location" header and should redirect.

    // end all output buffering so we can flush
   
while (ob_get_level()) {
     
ob_end_flush();
    }
   
   
flush();
   
   
sleep($latency);
  }
?>

Save this into a file on your webserver (eg. flush-test.php) and run it after every configuration change to see if output buffering got disabled or not.

Syndicate content