PHP: close browser connection and keep on executing

Close the browser's connection

Close the browser's connection

Sometimes you need to execute a really long process after displaying a web page. For example, you may need to evaluate the data a user submitted, and that operation may take a lot of time. And since you're a thoughtful programmer, you don't want make the user stare at a blank page while they wait for the process to finish.

A good solution is to display a confirmation page and then close the browser's connection to the web server. Closing the browser's connection will cause the browser to stop "spinning" and display a "Done" status. The user can then feel assured that their part in the matter is over with, and can navigate elsewhere. In the meantime, your PHP (or whatever language) continues executing on the server. You could even get fancy with it, and use AJAX to display the script's execution progress to the user on the confirmation page.

The key to doing this are the two HTTP header fields:

Connection: close
Content-Length: n (n = size of output in bytes )
The "Connection: close" header lets the browser know that it should not maintain a persistent connection. The "Content-Length: " header tells the browser how much data to expect. Thus, it's necessary to supply the size of the output (in bytes) using "Content-Length: ". Then the browser knows when it's downloaded everything and can terminate the connection.

Also, if sessions are enabled, it's necessary close to the current one, using session_write_close(). Otherwise, subsequent HTTP requests from the session user will hang. PHP locks session data while it's executing to prevent concurrent writes.

Example 1 - Basic Usage

 

<?php
// buffer all upcoming output
ob_start();
echo "Here's my awesome web page";

// get the size of the output
$size = ob_get_length();

// send headers to tell the browser to close the connection
header("Content-Length: $size");
header('Connection: close');

// flush all output
ob_end_flush();
ob_flush();
flush();

// close current session
if (session_id()) session_write_close();

/******** background process starts here ********/

Example 2 - embedded PHP with execution progress updates

View the demo

This basic example executes some arbitrary time consuming code in the background, after the user clicks the submit button. Notice that after clicking submit, the browser displays the web page and stops downloading. Also, as a bonus, the execution progress of the script is displayed periodically using Prototype's PeriodicUpdater ajax calls.

  • Lines 02 - 19:  AJAX handler that returns the progress of the background execution to the AJAX calls. It's basically reading the contents of the progress file to the browser.
  • Lines 22 - 35:  Creates a uniquely named temporary file to track the background process's execution. It also buffers all output (Line 034) so that we can calculate the size of all the content. The size will be sent in the "Content-Length: " header.
  • Lines 37 - 85:  HTML content. The javascript in Lines 68 - 80 setup a Prototype PeriodicUpdater. It checks on the status of the background script every 1 second.
  • Line 88:  Calculates the size of the buffered output in bytes.
  • Lines 90 - 101:  Sends the 2 necessary headers to the browser, flushes all buffered output to the browser, and if necessary, closes the session.
  • Lines 103 - n:  This is where the background process should be executed. In the example, some time consuming code is executed, and it's progress (ie. "Executing step n...") is written to a temporary progress file.

Demo source code

<?php
    // ajax updater handler
    if (isset($_POST['ajax']) && $_POST['pidFile']){
        // clean input
        $pidFile = preg_replace('/[^\w\d]/', '', $_POST['pidFile']);
        $pidFile = "../cache/$pidFile";

        if (! file_exists($pidFile)){
            header('HTTP/1.1 500 Internal Server Error');
            exit;
        }
        if(! $progress = file_get_contents($pidFile)){
            header('HTTP/1.1 500 Internal Server Error');
            exit;
        }

        echo nl2br($progress);
        exit;
    }

    // was form submitted?
    if(isset($_POST['submitted'])){
        // create a unique file to track script progress
        $pidFile = tempnam('../cache', '');

        // create the file for writing
        $fh = fopen($pidFile, 'wrx+');
        if (! $fh){
            print "failed to create the file";
            exit;
        }

        // buffer all upcoming output
        ob_start();
    }
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head profile="http://gmpg.org/xfn/11">
        <title>Zulius :: Close Browser Connection :: Example #1 Inline PHP</title>
        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js"></script>
        <style>
            body{ text-align: center; margin: 20px auto; font-family: Arial, Helvetica, sans-serif;}
            div#progress{ min-height: 220px; margin: 10px auto}
        </style>
    </head>
    <body>
        <h1>
        <?php
        if(isset($_POST['submitted'])){
            echo 'background process started';
        }
        else{
            echo 'click to start background process';
        }
        ?>
        </h1>
        <div id="progress">
        </div>
        <form method="POST">
            <input type="submit" name="submitted" value="Go!">
        </form>
    </body>
    <?php
        if(isset($_POST['submitted'])){
        // show progress in browser
    ?>
        <script type="text/javascript">
            updater = new Ajax.PeriodicalUpdater({success: 'progress'}, '<?php echo $_SERVER['REQUEST_URI'] ?>',
                                                { method: 'post',
                                                  frequency: 1,
                                                  decay: 1,
                                                  parameters: {
                                                               ajax: 1,
                                                               pidFile: '<?php echo basename($pidFile) ?>'
                                                             },
                                                  onFailure: function(){ this.stop(); }
                                                });
            setTimeout('updater.stop()', 15000);
        </script>
    <?php
        }
    ?>
</html>
<?php
if(isset($_POST['submitted'])){
    // get the size of the output
    $contentLength = ob_get_length();

    // these headers tell the browser to close the connection
    // once all content has been transmitted
    header("Content-Length: $contentLength");
    header('Connection: close');

    // flush all output
    ob_end_flush();
    ob_flush();
    flush();
    
    // close current session
    if (session_id()) session_write_close();

    /******** background process starts here ********/
    for($i=1;$i<=10;$i++){
        if(fwrite($fh, "Executing step $i...\n") == false){
            print "failed to write to the file: " . error_get_last();
            exit;
        }
        sleep(1);
    }

    fwrite($fh, "Done!\n");
    sleep(2);

    fclose($fh);

    // delete the progress file
    unlink($pidFile);

}

Example 3 - include embedded PHP file

If you've separated the HTML from your main script, it's very easy to close the browser connection. Just capture the output from your include command, get the size of the content, and send the headers and content:

// capture output of include file in buffer
ob_start();
include '/path/to/your/content/file';

// get the size of the output
$contentLength = ob_get_length();

// these headers tell the browser to close the connection
// once all content has been transmitted
header("Content-Length: $contentLength");
header('Connection: close');

// flush all output
ob_end_flush();
ob_flush();
flush();

// close current session
if (session_id()) session_write_close();

/******** background process code here ********/

Background Process

If your background process is vital and could take a lot of time, make sure to set these flags at the top of your script:ignore_user_abort(true); set_time_limit(0);

ignore_user_abort(true) will prevent the script from halting if the user happens to push the browser's stop button before the connection is closed. set_time_limit(0) will prevent your script from being terminated if it surpasses the max_execution_time set in php.ini.

Related post: Close browser connection with Zend Framework

Comments

  1. Thank you, very helpful, that's how I did http://dig.ua/ping/zulius.com

  2. PHP guy

    I had to also set header('Content-Encoding: none');
    I believe this has something to do with compression, gzip or something. It probably depends on whether your server does it by default. Probably a better way is to find the compressed length.

  3. CaMer0n

    My scripts had sessions active, which was preventing the above code from working.
    Adding session_write_close(); before the background task fixed it.

  4. @CaMer0n - thanks! I've updated this post.

  5. Ramzi

    I'v tried the Example 2 but the explorer doesn't close the connection until the process is finished .

    is their any configuration required in the Apache server or PHP files?

  6. @Ramzi - if mod_deflate is enabled on Apache, the HTTP response is getting gzipped before being sent to the client. This means PHP won't know the correct size of the response to put in the "Content-Length" header. Try disabling mod_deflate if it's enabled.

  7. That's awesome, didn't even know this was possible til now. Will be using with abandon. You rock.

  8. This is now working on a curled page. I am using curl in 1 php file to run a 2nd php file. I am using the above code trying to get the 2nd php file to send a response back to the 1st php file and keep working. The problem is that the 2nd php file isn't sending any response until it completes running, but that takes almost a minute. How can I send a response back to the 1st, calling php file then continue with the rest of the code and without requiring the 1st php file to wait for the 2nd file to finish?

  9. Hey thanks for the tutorial. I created a class which should help streamlining the execution of output and background functions.

    It even works with gzip.

    https://github.com/RobertBiehl/php-background-server

  10. Danny Tran

    This is awesome, thanks! :)

  11. andronick

    fastcgi_finish_request(); // important when using php-fpm!

  12. Yariv

    Thank you, it is very helpful

Leave a Comment