skip to content

Processes & Concurrency

Laravel's Process facade runs shell commands with timeouts, pools and pipes; Concurrency and defer() run work in parallel or after the response. Interviewers ask when a queued job fits better.

on this pageshow

explore

questions

4

In Laravel, what does Process::run() return when an external thumbnail command exits with a non-zero code, and how do you turn that into an exception?

level: juniorimportance: must knowfreq 42%

answer

  1. the type run() hands back
  2. successful() means exit code 0
  3. output() is stdout, errorOutput() stderr
  4. throw() raises ProcessFailedException
  5. 60-second default, ProcessTimedOutException

basics

~10 s

Process::run() returns an Illuminate\Process\ProcessResult whatever the exit code: failed() is true and exitCode() holds the code. Chain throw() to raise ProcessFailedException. Running past the 60-second default timeout always throws ProcessTimedOutException.

solid answer

~40 s

`Process::run()` blocks until the command finishes and returns a `ProcessResult`. A non-zero exit code does not throw: `successful()` is false, `failed()` is true, `exitCode()` holds the code, and `output()` and `errorOutput()` hold stdout and stderr. Chain `->throw()` (or `throwIf($condition)`) to raise `Illuminate\Process\Exceptions\ProcessFailedException`, whose public `$result` carries the failed result and whose message includes the command, exit code and output; on success `throw()` returns the result for chaining. Timeouts are different: a process that runs past its limit — 60 seconds by default, changed with `timeout()` or removed with `forever()` — throws `ProcessTimedOutException` with no `throw()` call. Passing the command as an array, such as `['magick', $in, '-thumbnail', '300x300', $out]`, keeps each value a single argument instead of shell text.

code

php · 16 lines
php
<?php

use Illuminate\Process\Exceptions\ProcessFailedException;
use Illuminate\Process\Exceptions\ProcessTimedOutException;
use Illuminate\Support\Facades\Process;

try {
    Process::timeout(20)
        ->run(['magick', $source, '-thumbnail', '300x300', $target])
        ->throw();
} catch (ProcessFailedException $e) {
    report($e); // message includes command, exit code, stdout and stderr
    $exitCode = $e->result->exitCode();
} catch (ProcessTimedOutException $e) {
    report($e); // stopped after 20 seconds; no throw() needed for this one
}

go deeper

for a junior

Remember that Process::run() returns a result you check with successful() or failed(), and that throw() turns a failure into an exception.

for a middle

Explain the ProcessResult methods, the ProcessFailedException and ProcessTimedOutException split, the 60-second default, and why the array command form matters.

for a senior

Show you run external tools defensively: explicit timeouts sized to real inputs, stderr in the logs, both exceptions handled, and no shell strings built from user data.

for a principal

Weigh shelling out against a native PHP library or a separate service: process calls add a binary to every server image, but can be the most reliable option for heavy media work.

## What run() gives back Laravel's `Process` facade (`Illuminate\Support\Facades\Process`) is a fluent wrapper around the Symfony Process component. `Process::run($command)` starts the command, **waits for it to finish**, and returns an `Illuminate\Process\ProcessResult`. The result is returned for every exit code; a failing command is data, not an exception. | Method | Returns | |---|---| | `successful()` | `true` when the exit code is 0 | | `failed()` | the opposite of `successful()` | | `exitCode()` | the numeric exit code | | `output()` | everything the command wrote to stdout | | `errorOutput()` | everything it wrote to stderr | | `seeInOutput('x')` / `seeInErrorOutput('x')` | whether a string appears in either stream | | `command()` | the command line that ran | For a thumbnail generator, the tool's own error message — "unable to open image", "no decode delegate" — usually lands in `errorOutput()`, and the exit code is the only reliable signal that something went wrong. ## Turning failure into an exception Call `throw()` on the result: if the exit code is non-zero it throws **`Illuminate\Process\Exceptions\ProcessFailedException`**, otherwise it returns the same result so you can keep chaining (`Process::run($cmd)->throw()->output()`). Details worth knowing: - `throwIf(bool $condition)` throws only when the condition is true **and** the process failed. - Both accept a closure that runs with the result and the exception just before it is thrown. - The exception's public `$result` property holds the `ProcessResult`, and its message contains the command, the exit code, and the stdout and stderr text, so the log entry is useful on its own. - The exception code is the process exit code. ## Timeouts are always exceptions Every pending process starts with a **60-second** timeout. A command that runs longer is stopped and Laravel throws **`Illuminate\Process\Exceptions\ProcessTimedOutException`** — with or without `throw()`, because a killed process has no meaningful exit code to report. - `timeout(120)` raises the limit; it also accepts a `CarbonInterval`, so `timeout(minutes(2))` with `use function Illuminate\Support\minutes;` reads well. - `idleTimeout(30)` limits how long the process may go **without producing output**; exceeding it throws a subclass of `ProcessTimedOutException`. - `forever()` removes the limit entirely — rarely right in a web request. - The exception carries a public `$result` too, holding whatever output was captured before the kill. ## Passing the command safely `run()` accepts either form: 1. **An array** — `['magick', $source, '-thumbnail', '300x300', $target]`. Each element is handed to Symfony Process as one argument, so a filename containing spaces or shell characters stays a filename. 2. **A string** — `'magick photo.jpg -thumbnail 300x300 thumb.jpg'`. It is interpreted as a shell command line, which is what you want for pipes or redirects and exactly what you do not want around user-supplied names. Other options chain before `run()`: `path($dir)` sets the working directory (default: the current one), `env([...])` adds environment variables, `input($data)` feeds stdin, and `quietly()` discards output to save memory. ## Reading output while it runs `run()` accepts a closure as its second argument. It is called with the output type — `stdout` or `stderr` — and each chunk as the command produces it, so an Artisan command can stream a long conversion's progress to the console instead of waiting for the end: ```php Process::run($command, function (string $type, string $output) { echo $output; }); ``` For commands that print a lot you do not need, `quietly()` disables output capture entirely and saves memory. `env(['LOAD_PATH' => false])` removes an inherited environment variable, since child processes otherwise inherit the parent's environment. ## A thumbnail example ```php $result = Process::timeout(20) ->run(['magick', $source, '-thumbnail', '300x300', $target]); if ($result->failed()) { Log::warning('Thumbnail failed', [ 'exit' => $result->exitCode(), 'error' => $result->errorOutput(), ]); } ``` ## Pitfalls - Treating a returned result as success without checking `successful()` or calling `throw()`. - Reading `output()` for the error text when the tool writes it to stderr. - Building the command string with interpolated user input instead of the array form. - Leaving the 60-second default on a slow conversion, then being surprised by `ProcessTimedOutException` on large images. - Catching only `ProcessFailedException` and letting timeouts escape — the two exceptions are separate classes.

  • What is the difference between Process::run('magick '.$name.' ...') and passing an array to run()?
    A string is parsed as a shell command line, so characters in `$name` can change what runs. An array is passed to Symfony Process element by element, each becoming exactly one argument. Use the array form whenever any part of the command comes from a user or a file name.
  • How do you let a long video transcode in an Artisan command run without Laravel's process timeout?
    Chain `forever()`, which removes the timeout, or set a generous `timeout()` such as `timeout(minutes(30))`. `idleTimeout()` can still catch a process that hangs without producing output.

saying these in an interview costs you the question

  • Process::run() throws an exception whenever the command exits with a non-zero code.
  • Laravel processes have no timeout unless you set one.
  • A timeout only throws if throw() was chained onto the result.
  • Error messages from the external tool are always in output().
  • Passing the command as one interpolated string is as safe as the array form.
open as a page

In Laravel, when does a closure passed to defer() run, when is it skipped, and when should the work be a queued job instead?

level: seniorimportance: must knowfreq 38%

basics

~20 s

defer() runs a closure after the response is sent, or after a command or queued job ends, and only on success (status below 400) unless always() is chained. Nothing retries it, so work that must happen belongs in a queued job.

open as a page

In Laravel's Process facade, when do you use start(), Process::pool() or Process::pipe() instead of run(), and what does each give back?

level: middleimportance: should knowfreq 28%

basics

~20 s

start() launches a command in the background and returns an InvokedProcess to poll or wait() on. Process::pool() starts several commands at once and wait() returns their results by key. Process::pipe() feeds each command's output into the next and returns one result.

open as a page

In Laravel 13, how does Concurrency::run() execute three slow API calls at once with the default process driver, and what does that require of the closures?

level: seniorimportance: should knowfreq 30%

basics

~20 s

With the process driver, Concurrency::run() serializes each closure, runs it in its own PHP process through a hidden Artisan command, and unserializes the results by position or key. Closures, captured variables and return values must be serializable.

open as a page