PHP CLI vs PHP-FPM vs built-in server
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 limit —
max_execution_time = 0by 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 —
echogoes directly to stdout - STDIN/STDOUT/STDERR are available via
STDIN,STDOUT,STDERRconstants - 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_childrencontrols concurrency; each worker handles one request at a time max_execution_timedefaults to 30 seconds — requests taking longer are killedmemory_limitdefaults 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.
| Feature | CLI | PHP-FPM | Built-in Server |
|---|---|---|---|
| Use case | Scripts, workers | Web traffic | Local dev |
| Concurrency | Single process | Multi-process pool | Single-threaded |
| max_execution_time | 0 (unlimited) | 30s default | 0 (unlimited) |
| Runs as | Current user | www-data | Current user |
| OPcache by default | No | Yes | No |
| HTTP superglobals | No | Yes | Yes |
Code Example
<?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.