skip to content

Why does a PHP cron script run through the CLI never hit max_execution_time, while the same code times out on the web?

level: middleimportance: should knowfreq 48%

answer

  1. CLI hardcodes some ini values
  2. max_execution_time=0 means no limit
  3. web default is 30 seconds
  4. html_errors=0, implicit_flush=1, output_buffering=0
  5. php.ini cannot override, -d can

basics

~20 s

The CLI SAPI hardcodes max_execution_time=0, meaning no limit, and php.ini cannot change that; web SAPIs use the configured value, 30 seconds by default. The same script is unlimited from cron and cut off on the web.

solid answer

~40 s

The CLI SAPI applies a few **hardcoded ini values** after reading the configuration files: `max_execution_time=0` (no limit), `max_input_time=-1`, `html_errors=0`, `implicit_flush=1` and `output_buffering=0`. Because they are applied after php.ini, a `max_execution_time = 30` line there has no effect on the CLI, while a web SAPI such as `fpm-fcgi` uses it, and 30 seconds is also the built-in default. So a newsletter send that takes four minutes finishes from cron and dies on the web with `Maximum execution time of 30 seconds exceeded`. A `-d max_execution_time=300` on the command line or `set_time_limit()` in the script can still set a limit in the CLI. The CLI also defaults `display_errors` to on and does not change to the script's directory.

code

bash · 5 lines
bash
php -r 'var_dump(ini_get("max_execution_time"));'
# string(1) "0"   (CLI: unlimited, whatever php.ini says)

php -d max_execution_time=300 send-queue.php
# -d is applied after the CLI's hardcoded values

go deeper

for a junior

Remember that CLI scripts have no execution time limit by default while web requests default to 30 seconds.

for a middle

List the CLI's hardcoded ini values, explain why php.ini cannot override them, and show how -d or set_time_limit() still sets a limit.

for a senior

Move long work out of web requests, give cron jobs explicit limits and locks, and remember relative paths and ini files differ between cron and the web.

for a principal

Set a policy for long-running work: which jobs run from cron or workers, which timeouts bound them, and how overlapping runs are prevented.

## Same code, two behaviours A newsletter tool sends queued mail in batches. When cron runs `php send-queue.php`, a large batch takes four minutes and completes. When an admin clicks "Send now" in the web UI, which includes the same code, the request dies after thirty seconds with: `Fatal error: Maximum execution time of 30 seconds exceeded` Nothing in the code differs. The **SAPI** does. ## The CLI's hardcoded ini values The CLI SAPI overrides a handful of php.ini directives because their web defaults make no sense in a shell. The manual lists them, and php-src's CLI source applies them after all configuration files have been parsed: | Directive | CLI value | Why | |---|---|---| | `max_execution_time` | `0` (unlimited) | shell scripts are often long-running | | `max_input_time` | `-1` | there is no HTTP request body to read | | `html_errors` | `0` | HTML tags clutter a terminal | | `implicit_flush` | `1` | output should appear immediately | | `output_buffering` | `0` | same reason; buffering functions still work | Because these values are applied **after** php.ini, a line such as `max_execution_time = 30` in the file has no effect on the CLI. The manual adds that they can still be changed at runtime, and options given with `-d` on the command line are applied after the hardcoded list, so `php -d max_execution_time=300 send-queue.php` sets a limit. ## What the web SAPIs use Under `fpm-fcgi`, `apache2handler` or `cgi-fcgi`, `max_execution_time` comes from configuration. Its built-in default is `30` seconds, and the shipped `php.ini-production` also sets `30`. That is why the web path fails where cron succeeds. ## Other CLI defaults worth knowing - **`display_errors`** defaults to on in the CLI, so errors print to the terminal; a php.ini can still override this one, unlike the hardcoded list above. - **Working directory.** The CLI does not change to the script's directory. Relative paths resolve against wherever cron started the process, often the user's home directory, which is why cron scripts should build paths from `__DIR__`. - **Configuration files.** The CLI may load a different php.ini from the web SAPI, depending on how PHP was installed; `php --ini` shows which files the CLI read. - **No HTTP.** There are no request headers, cookies, `$_GET` or `$_POST`. - **Only the `cli` SAPI.** The hardcoded list is applied to the plain CLI, not to the `php -S` development server (`cli-server`), which keeps php.ini's time limit. ## Fixing the newsletter The right fix depends on what the web button should do: 1. **Do not run long work in a web request.** The button should enqueue the send and return immediately; the cron job, or a queue worker, does the sending. 2. **If it must run on the web**, raise the limit for that one script with `set_time_limit()`, knowing that FPM and the web server have their own timeouts that PHP's setting does not lift. 3. **For the CLI**, consider setting an explicit limit with `-d max_execution_time=...` or `set_time_limit()` so a stuck job cannot run forever. ## A note on unlimited time An unlimited CLI script is convenient until one hangs. A cron job that runs every five minutes and never finishes will pile up overlapping copies, each holding memory and connections. Long CLI jobs usually combine an explicit limit, a lock so only one copy runs at a time, and logging of progress. ## Checking what a SAPI actually uses When the two environments disagree, compare them directly rather than guessing: - in the terminal, `php -r 'var_dump(ini_get("max_execution_time"));'` prints the CLI's effective value; - in the web app, a temporary admin-only page that prints `ini_get('max_execution_time')` and `PHP_SAPI` shows the web SAPI's value; - `php --ini` lists the files the CLI loaded, which may not be the ones the web SAPI loaded. The pair of readings usually explains the difference in one step: `0` in the terminal, `30` on the web.

  • Can you give the CLI a time limit if php.ini cannot?
    Yes. Pass -d max_execution_time=300 on the command line, which is applied after the CLI's hardcoded values, or call set_time_limit() inside the script. Both are useful to stop a hung cron job from running forever.
  • Why does a cron script fail to find its config file with a relative path?
    The CLI does not change the working directory to the script's directory. Cron starts the process somewhere else, often the home directory, so relative paths resolve there. Build paths from __DIR__ instead.
  • Which other CLI defaults differ from a web SAPI?
    html_errors is off, implicit_flush is on and output_buffering is off, all hardcoded, and display_errors defaults to on. There is also no HTTP input, so $_GET, $_POST and cookies are empty.

saying these in an interview costs you the question

  • Setting max_execution_time in php.ini limits CLI scripts too.
  • The CLI and the web server always read the same php.ini.
  • max_execution_time = 0 means scripts are killed immediately.
  • The CLI changes to the script's directory before running it.
  • The CLI's hardcoded values cannot be changed at all.