php runtime and server environment

Professional Runtime Orientation

Professional PHP runs in more than one shape. The same application may use PHP-FPM for web requests, CLI for console commands, workers for queues, cron for scheduled jobs, and a long-running runtime for high-throughput HTTP or websocket work.

The runtime matters because it decides how code starts, how long memory lives, how configuration is loaded, how failures are logged, and whether state is reset between requests.

The Common Production Shape

A traditional PHP web request usually flows like this:

browser
  -> Nginx / Apache / Caddy
  -> PHP-FPM worker
  -> framework front controller
  -> response

The web server handles TLS, static files, request buffering, and routing PHP requests to PHP-FPM. PHP-FPM keeps a pool of PHP worker processes ready to execute requests.

In this model, each request gets a fresh application lifecycle. After the response, most in-memory state is discarded. That request/response reset is one reason classic PHP is forgiving for beginners.

CLI Is A Different Runtime

CLI PHP runs commands in a terminal or process manager. It is used for Composer, framework commands, migrations, queue workers, cron jobs, import scripts, and maintenance tools.

php -v
php framework-command migrate
php bin/console cache:clear
php worker.php

A small diagnostic can prove which runtime is executing:

PHP example
<?php

declare(strict_types=1);

echo 'SAPI: ' . PHP_SAPI . PHP_EOL;
echo 'Memory limit: ' . ini_get('memory_limit') . PHP_EOL;
echo 'APP_ENV: ' . (getenv('APP_ENV') ?: 'not set') . PHP_EOL;

// Example CLI output:
// SAPI: cli
// Memory limit: 128M
// APP_ENV: not set

CLI and FPM can load different php.ini files and different environment variables. When a command works but the website fails, compare the runtime first.

PHP-FPM Workers

PHP-FPM manages worker processes. Each worker can handle many requests over its lifetime, but the application code normally runs one request at a time in that worker.

Important FPM concepts:

  • pools define groups of workers with their own user, limits, and configuration
  • pm.max_children limits how many requests can run at once in a pool
  • slow logs help find requests that take too long
  • status pages help show busy and idle workers
  • web server configuration must pass the correct script filename to FPM

FPM problems often appear as 502 Bad Gateway, timeouts, slow requests, or PHP files being downloaded instead of executed.

Queue Workers And Cron

Queue workers and cron scripts are CLI processes, but they behave differently from one-off commands.

A migration command starts, does work, and exits. A queue worker may stay alive for hours and process thousands of jobs.

PHP example
<?php

declare(strict_types=1);

while (true) {
    echo 'Process one job' . PHP_EOL;
    sleep(1);
}

That loop is intentionally simple, but it shows the important difference: memory and static state can survive between jobs. A leak that is harmless in one web request can become serious in a long-running worker.

Long-Running Runtimes

Tools such as RoadRunner, FrankenPHP worker mode, Swoole, OpenSwoole, and Laravel Octane keep PHP applications in memory between requests.

That can improve performance because the framework does not need to bootstrap from scratch on every request. The tradeoff is that application state can leak between users if code assumes the classic request/response reset.

This is safe:

PHP example
<?php

declare(strict_types=1);

function greetingFor(string $name): string
{
    return 'Hello ' . $name;
}

echo greetingFor('Ada') . PHP_EOL;

// Prints:
// Hello Ada

This is risky in a long-running runtime:

PHP example
<?php

declare(strict_types=1);

final class CurrentUser
{
    public static ?int $id = null;
}

CurrentUser::$id = 42;

echo 'Current user: ' . CurrentUser::$id . PHP_EOL;

// Prints:
// Current user: 42

Static request-specific state can survive longer than expected. In long-running runtimes, per-request data should usually live in request objects, scoped services, or explicit context that is reset after each request.

Event Loops

ReactPHP and Amp use event loops for asynchronous work. The idea is that one process can manage multiple non-blocking operations without waiting idly for each one to finish.

Event-loop code is powerful for sockets, websockets, streaming, and high-concurrency I/O. It also changes how you think about blocking calls.

In an event-loop application, a normal blocking database query, file read, or HTTP request can pause the whole loop unless it uses compatible non-blocking APIs.

You do not need to start with event loops as a junior PHP developer. You do need to recognise them so you do not accidentally add blocking code to a non-blocking application.

Runtime Choice Changes Debugging

When a production issue appears, first identify the runtime:

  • browser request through FPM
  • CLI command
  • queue worker
  • cron job
  • websocket server
  • event-loop process
  • long-running HTTP worker

Then ask runtime-specific questions:

  • Which php.ini did it load?
  • Which user owns the process?
  • Which environment variables are available?
  • Does memory reset after each request or job?
  • Where are errors logged?
  • Is the process supervised by systemd, Supervisor, Docker, Kubernetes, or something else?
  • Does changing code require a reload or worker restart?

Those questions often find the bug faster than reading application code first.

What You Should Be Able To Do

After this lesson, you should be able to describe the difference between CLI, PHP-FPM, queue workers, long-running runtimes, and event-loop runtimes. You should also be able to explain why state leakage is a real risk when PHP processes stay alive.

For junior PHP work, this matters because production PHP is not just index.php. You need to know what kind of process is running your code before you can debug it professionally.

Practice

Practice: Identify The Runtime Before Debugging

Create a short runtime investigation note for a PHP application.

Task

Write a checklist for an application that has:

  • normal web requests through Nginx and PHP-FPM
  • a queue worker started from CLI
  • a scheduled cron command
  • a possible long-running worker runtime

For each runtime, list:

  • how you would prove it is the code path being used
  • which configuration or environment values you would check
  • what kind of state-leak or restart issue could happen

Include one small PHP diagnostic script or snippet that prints PHP_SAPI, memory_limit, and APP_ENV.

Afterward, add a short note explaining why the same PHP code can behave differently in CLI and web runtimes.

Show solution

This solution starts with a small diagnostic, then maps the checks to each runtime.

PHP example
<?php

declare(strict_types=1);

echo 'SAPI: ' . PHP_SAPI . PHP_EOL;
echo 'Memory limit: ' . ini_get('memory_limit') . PHP_EOL;
echo 'APP_ENV: ' . (getenv('APP_ENV') ?: 'not set') . PHP_EOL;

// Example CLI output:
// SAPI: cli
// Memory limit: 128M
// APP_ENV: local

For web requests through Nginx and PHP-FPM, prove the path by hitting a known route and checking the web server and FPM logs. Check the FPM pool, loaded php.ini, document root, SCRIPT_FILENAME, environment variables, and whether PHP errors are logged where you expect.

For a queue worker, prove the path by dispatching a known test job and checking the worker log. Check the CLI PHP version, CLI php.ini, supervisor configuration, memory limit, environment variables, retry settings, and whether the worker needs a restart after deployment.

For cron, prove the path by checking the crontab entry and writing a timestamped log line from the command. Check the user that runs the cron, working directory, PATH, environment variables, and absolute paths.

For a long-running worker runtime, prove the path through the runtime's status command, process list, logs, or a request that reports the runtime name. Check whether code changes require reloads, whether request-specific state is reset, and whether static properties or singletons can leak data between requests.

The same PHP code can behave differently in CLI and web runtimes because they may use different SAPIs, configuration files, users, working directories, extensions, environment variables, process lifetimes, and logging paths.