Table of Contents

PHP RFC: Wall-Clock Time Based Execution Timeout

Introduction

On most platforms, PHP currently measures the request timeout based on CPU time, rather than wall-clock time, so neither sleep(), nor network or system calls count towards the limit of the max_execution_time ini setting. This behavior is not only surprising, but it can have serious consequences for high traffic application, where terminating requests which take too long time to process is essential for avoiding cascading failures.

Even if each individual network/system calls have their own timeout, execution time can still horribly go out of control when there are hundreds or even thousands of such calls during the same request. This risk also applies to CLI scripts which can possibly execute millions of database queries or API requests.

Let's consider the following piece of code to better illustrate the problem:

<?php
 
ini_set("max_execution_time", 10);
 
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://example.com/index.php");
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
 
for ($i = 0; $i < 100; $i++) {
    curl_exec($ch);
}

Even though max_execution_time as well as CURLOPT_TIMEOUT are correctly set, this script can possibly run for ~100 seconds, given the cURL requests always time out. We can also run into the same problem while communicating through sockets, and not even setting the default_socket_timeout ini setting to a low value can help.

To make things even worse, none of the most popular web servers offer a convenient solution. I.e. when using PHP-FPM as process manager, the request_terminate_timeout pool-level config option is available for stopping execution after a certain amount of time at latest. This can help, but it still falls short when there is a wide variety of acceptable timeout settings for the individual scripts. So in the end, one would have to maintain different pools for slow and fast scripts... Clearly, this can quickly become a burden. Not to mention the fact that neither shutdown handlers, nor the RSHUTDOWN internal function* is invoked in case of a PHP-FPM timeout, so any extensions that do monitoring are rendered useless.

(*) RSHUTDOWN internal function: it is used by extensions to clean up their resources after each request.

Another solution for the problem could be using something like:

if (time() - $startTime > $timeout) {
    die("Timeout exceeded");
}

But let's just leave this idea aside...

Proposal

This RFC proposes to add a max_execution_wall_time ini setting. If a script runs longer than the value of max_execution_wall_time, measured in seconds according to wall-clock (or real) time, a fatal error is raised, similarly to what happens when exceeding “max_execution_time”. By default, the value of the new ini setting is 0, which means that the allowed script duration is unlimited.

A limitation of the implementation is that the timeout takes into effect on a best-effort basis, meaning that the fatal error is triggered only after the call exceeding the time limit is finished. This is in line with the current timeout behavior, and the RFC considers this as an acceptable limitation.

Furthermore, there is a question in case of platforms which already measure max_execution_time based on wall-clock time. The most notable of such systems is definitely Windows, but IBM PASE and Cygwin are also affected, since they both only support real-time timers. The position of this RFC is that max_execution_wall_time should act as an “alias” of max_execution_time on these platforms, so any changes to the settings in question would affect the same timer.

Adding a set_time_limit() counterpart is out of scope of the current RFC.

Alternatives

HHVM solved the same problem by introducing the TimeoutsUseWallTime ini setting (https://github.com/facebook/hhvm/commit/9a9b42e3610cdf242f16ddb8936ce34adfa0be9e) in order to offer a way to change the meaning of max_execution_time, while still (partially) remaining compatible with PHP. This made sense for a sister language, but in case of PHP itself, it seems to be an unnecessary complication which could result in weird issues.

It would also be possible to directly change the behavior of max_execution_time to measure wall-clock time on all platforms. This approach is rejected by the current RFC mainly due to its vast BC break, and the fact that CPU-time based execution timeouts are useful as a sanity check of the code itself (e.g. for making sure there are no infinite loops).

Backward Incompatible Changes

None.

Vote

Add the max_execution_wall_time ini setting?

The vote requires 2/3 majority to be accepted.