In Laravel Octane, what does max_execution_time in config/octane.php do, and how does each server enforce it on a slow request?
answer
- default 30 seconds, 0 disables
- handed over at server start
- Swoole: SIGKILL worker, 408
- RoadRunner: supervisor exec_ttl
- FrankenPHP: set_time_limit on Linux
basics
~20 smax_execution_time caps how long Octane lets one request run: 30 seconds by default, 0 for no limit, fixed when the server starts. Swoole kills the stuck worker and answers 408, RoadRunner gets it as exec_ttl, FrankenPHP applies set_time_limit on Linux.
solid answer
~40 s`config/octane.php` ships `'max_execution_time' => 30`; `0` disables the limit, and a change needs a full server restart. Octane passes it to each server differently. On Swoole, when the value is above 0 Octane keeps a table of each worker's current request and a once-a-second timer in the master; a request older than the limit gets its worker `SIGKILL`ed and the client a `408` response. On RoadRunner it becomes `http.pool.supervisor.exec_ttl`, RoadRunner's hard limit on one job. On FrankenPHP it is exported as `REQUEST_MAX_EXECUTION_TIME` and Octane's worker script calls `set_time_limit()` with it, but only on Linux. In every case the request dies abruptly, so long work belongs in a queue, not an HTTP request.
code
php · 8 lines<?php
// config/octane.php (excerpt)
return [
// Seconds one request may run; 0 disables the limit.
// Restart the Octane server after changing it.
'max_execution_time' => 30,
];go deeper
Recall that Octane has its own request time limit, 30 seconds by default, and that 0 turns it off.
Explain that the limit is set when the server starts, needs a restart to change, and is enforced differently by Swoole, RoadRunner and FrankenPHP.
Show you design for abrupt kills: no cleanup guaranteed, side effects half done, long work in queues, and timeouts layered below the Octane limit.
Weigh a global per-request budget against per-endpoint needs, and decide which slow paths get redesigned as asynchronous work instead of a higher limit.
## What the setting is A long-lived worker that gets stuck on one request - a hung upstream call, a runaway loop - is a worker that serves nobody else. **Laravel Octane** therefore has its own per-request time limit in `config/octane.php`: ```php 'max_execution_time' => 30, ``` - The default is **30 seconds**. - `0` disables the limit. - The value is handed to the server **when it starts**. Octane's docs say that after changing it you must restart the Octane server; `octane:reload` is not enough. This is Octane's setting, separate from PHP's `max_execution_time` ini directive, although one of the drivers implements it with PHP's time-limit function. ## How each server enforces it | Server | Mechanism | What the client sees | |---|---|---| | Swoole / Open Swoole | a timer in the master checks a shared table of running requests every second and `SIGKILL`s the worker process past the limit | a `408` response written by the server | | RoadRunner | Octane passes `http.pool.supervisor.exec_ttl=<n>s`, RoadRunner's hard limit on a single job, and RoadRunner's supervisor stops the worker | an error from RoadRunner | | FrankenPHP | Octane exports `REQUEST_MAX_EXECUTION_TIME`; its worker script calls `set_time_limit()` with it **only when `PHP_OS_FAMILY === 'Linux'`** | the request fails with PHP's time-limit fatal error | ### Swoole in detail When the limit is above 0, Octane creates a **timer table** (a Swoole table) at server start. Each time a worker takes a request, it records its worker PID, the current time and the connection's file descriptor. Once a second, the master process walks the table. For any row older than the limit whose connection is still open and still the same request, it: 1. deletes the row; 2. sends `SIGKILL` to that worker process; 3. writes a `408` status to the client connection and closes it. `SIGKILL` cannot be caught: no `finally` block, no `terminating` callback and no Octane cleanup listener runs for that request. Swoole's manager then starts a replacement worker, which boots the application again. ### FrankenPHP in detail The worker script reads `REQUEST_MAX_EXECUTION_TIME` and calls `set_time_limit((int) $value)` - but only on Linux. On macOS or Windows development machines no time limit is applied through this path, so a test of the limit on a laptop can pass while production behaves differently. When the limit fires, the worker script ends with a fatal error (Octane's log handling treats exit status 255 as a request timeout), and FrankenPHP restarts crashed worker scripts. ## Consequences for application code - **Work may be half done.** A killed request can leave an external side effect without its follow-up. Database transactions that were open are rolled back by the database when the connection closes, but calls to other services are not undone. - **Cleanup code is not guaranteed.** Do not rely on `finally` or terminating callbacks for correctness in requests that might hit the limit. - **Long work belongs elsewhere.** Reports, imports and slow third-party calls should run in a queued job, with its own timeout, and the request should return quickly. - **Layer the timeouts.** Give outbound HTTP calls their own shorter timeouts so the application fails gracefully before Octane's hard limit kills the worker, and keep the proxy's read timeout (for example, Nginx's) above the Octane limit so the proxy does not give up first with a less useful error. ## Observing timeouts Because the request is killed rather than failed, the usual application error reporting sees nothing. Watch instead for `408` responses and restarted workers in the server output on Swoole, for time-limit fatal errors in FrankenPHP's logs, and for RoadRunner's worker errors, and alert on them alongside the proxy's upstream-timeout counts. ## Choosing a value - Leave the default for typical APIs and pages. - Raise it only for known slow endpoints that cannot be moved to a queue, and accept that every worker can then be held longer. - Setting `0` removes the safety net entirely; a single hung upstream can then occupy workers until the pool is exhausted.
- Why can't a finally block log that a Swoole request timed out under Octane?Octane enforces the limit on Swoole by sending `SIGKILL` to the worker process. That signal cannot be caught, so PHP never unwinds the stack; no `finally`, destructor or terminating callback runs. Record timing from the proxy or the client side, or use shorter timeouts inside the code that fail with a catchable exception.
- A developer tests the limit on a Mac with FrankenPHP and the request never stops; why?Octane's FrankenPHP worker script applies `set_time_limit()` only when `PHP_OS_FAMILY` is `Linux`. On macOS no limit is set through that path, so the slow request runs to completion. Test the behaviour in a Linux container that matches production.
saying these in an interview costs you the question
- octane:reload applies a new max_execution_time immediately
- Octane's limit is just PHP's max_execution_time ini on every server
- A timed-out Swoole request still runs its finally blocks and listeners
- Setting max_execution_time to 0 makes requests fail immediately
- FrankenPHP enforces the limit the same way on macOS and Linux