0

PHP CLI vs PHP-FPM vs built-in server

Beginner5 min read·php-01-003
interview

Concept

PHP doesn't run in one mode — it has multiple SAPIs (Server Application Programming Interfaces), each designed for a different execution context. The three you must know are the CLI, PHP-FPM, and the built-in development server. Confusing them explains bizarre debugging sessions where "it works in the browser but not in artisan."

PHP CLI (Command-Line Interface)

The CLI SAPI is invoked when you run php script.php or php artisan. Key characteristics:

  • No request lifecycle — no $_GET, $_POST, $_SERVER['HTTP_HOST'] by default
  • No execution time limitmax_execution_time = 0 by default (runs forever unless you set it)
  • No web user — runs as the current OS user, which has different permissions than www-data
  • Ignores output buffering from web contexts — echo goes directly to stdout
  • STDIN/STDOUT/STDERR are available via STDIN, STDOUT, STDERR constants
  • OPcache is off by default unless you explicitly set opcache.enable_cli=1

Use CLI for: Artisan commands, queue workers, cron jobs, migrations, data imports, scripts.

PHP-FPM (FastCGI Process Manager)

PHP-FPM is a long-running process manager that keeps a pool of PHP worker processes alive, waiting for requests from a web server (Nginx, Caddy, Apache via proxy_fcgi). Communication happens over a Unix socket (faster, same machine) or TCP socket.

Key characteristics:

  • Shared-nothing: each request is isolated — globals reset between requests
  • Process pools: pm.max_children controls concurrency; each worker handles one request at a time
  • max_execution_time defaults to 30 seconds — requests taking longer are killed
  • memory_limit defaults to 128MB — PHP-FPM workers are killed on overflow
  • Runs as www-data (or configured user) — file permission issues are almost always FPM-vs-CLI user mismatches

FPM configuration lives in /etc/php/8.4/fpm/pool.d/www.conf. The most important tuning parameters are pm.max_children, pm.start_servers, pm.min_spare_servers, pm.max_spare_servers.

Built-in Development Server

php -S localhost:8000 -t public/ starts a single-threaded HTTP server built into PHP. It is not production-ready — it handles one request at a time, has no FPM worker pools, no Unix sockets, no Nginx features. It is convenient for quickly testing a script without setting up Nginx or Apache.

Laravel's php artisan serve uses this built-in server internally.

FeatureCLIPHP-FPMBuilt-in Server
Use caseScripts, workersWeb trafficLocal dev
ConcurrencySingle processMulti-process poolSingle-threaded
max_execution_time0 (unlimited)30s default0 (unlimited)
Runs asCurrent userwww-dataCurrent user
OPcache by defaultNoYesNo
HTTP superglobalsNoYesYes

Code Example

php
<?php
declare(strict_types=1);

// Detecting which SAPI is running — useful for conditional behavior
$sapi = PHP_SAPI;

echo match(true) {
    str_starts_with($sapi, 'cli')    => "Running via CLI — no HTTP context\n",
    $sapi === 'fpm-fcgi'             => "Running via PHP-FPM — web request context\n",
    $sapi === 'cli-server'           => "Running via built-in dev server\n",
    default                          => "Unknown SAPI: {$sapi}\n",
};

// Practical example: Laravel does this to detect console context
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
    // Running as artisan or queue worker — set up CLI-specific behavior
    // e.g., disable session handling, output directly to terminal
    ini_set('memory_limit', '512M'); // Higher limit for data processing
}

// PHP-FPM specific: reading request context safely
if (PHP_SAPI === 'fpm-fcgi') {
    $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    $requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN';
    echo "Request from {$remoteAddr} via {$requestMethod}\n";
}

// Built-in server: PHP_SAPI === 'cli-server'
// The built-in server also serves static files if they exist
// It returns false from the router script to indicate "serve the file as-is"
if (PHP_SAPI === 'cli-server') {
    $file = __DIR__ . '/public' . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
    if (is_file($file)) {
        return false; // Serve the static file
    }
}

Interview Q&A

Q: What is PHP-FPM and why does Nginx use it instead of running PHP directly?

PHP-FPM is a FastCGI process manager that maintains a pool of persistent PHP worker processes. Nginx is a web server designed around non-blocking I/O for serving static content and proxying; it has no built-in PHP execution capability. When a PHP file is requested, Nginx passes the request to PHP-FPM over a Unix socket using the FastCGI protocol, PHP-FPM assigns it to an idle worker, the worker executes the script and returns the response, then the worker resets its state and becomes idle again. This separation means Nginx handles thousands of concurrent connections efficiently while PHP-FPM handles the CPU-intensive PHP execution in its own process pool. Tuning pm.max_children to match available memory (total_memory / memory_per_worker) is the single most important PHP-FPM optimization.


Q: Why does php artisan behave differently from the same code running under Nginx?

php artisan uses the CLI SAPI which runs as the current user (e.g., ubuntu), while Nginx/FPM runs as www-data. This causes permission issues on storage/ and bootstrap/cache/. Additionally, the CLI php.ini is separate from FPM's — memory limits, extension settings, and error reporting may differ. The CLI has no max_execution_time limit, so long operations that time out under FPM work fine via artisan. Environment variables set in the shell are visible to CLI but not to FPM (FPM reads .env via the application, not the shell environment). This is why migrations, queue workers, and scheduled tasks should always run via CLI, and why you should configure storage permissions for both users.


Q: When would you use the built-in PHP server versus setting up Nginx+FPM for local development?

The built-in server (php -S) is appropriate when you need a quick sanity check on a file or testing a standalone script with HTTP context — it requires no configuration. However, it processes one request at a time, which means loading a page with 20 assets (CSS, JS, images) blocks each file until the previous finishes. For any Laravel project, php artisan serve (which uses the built-in server) works acceptably in development but can feel slow on pages with many assets. Nginx + FPM (via Laravel Herd, Valet, or Docker) gives you concurrent request handling, closer production parity, virtual hosts, SSL, and proper PHP-FPM tuning. For serious day-to-day development, Herd or a Docker Compose setup is preferable.