PHP Runtime And Server Environment

Event Loop PHP

An event loop is a long-running coordinator that waits for work to become ready, then runs the matching callback or continuation. It is common in programs that handle sockets, timers, streams, child processes, signals, and many concurrent I/O operations without creating a new PHP process for each task.

PHP developers usually meet PHP through PHP-FPM or a short CLI command. In that model, the process receives one request or one command, does its work, sends a response, and exits or returns to a pool. Event-loop PHP is different. The process stays alive, the loop keeps scheduling work, and every callback shares the same process memory until the program stops.

That difference changes the engineering rules. Blocking one callback can delay unrelated connections. Keeping request data in a static property can leak into later work. Forgetting to remove a timer or stream listener can slowly grow memory. Event-loop PHP can be powerful, but it is a runtime model, not a faster spelling of ordinary PHP.

A Small Mental Model

This example is not a real event loop, but it shows the scheduling idea: a queue contains work, the process takes one item, runs it, then takes the next item.

PHP example
<?php

declare(strict_types=1);

$queue = [
    static fn (): string => 'read from socket',
    static fn (): string => 'write response',
    static fn (): string => 'run timer callback',
];

while ($task = array_shift($queue)) {
    echo $task() . PHP_EOL;
}

// Prints:
// read from socket
// write response
// run timer callback

A real event loop does not simply shift functions from an array. It asks the operating system which sockets are readable or writable, tracks timers, runs deferred callbacks, and keeps the process alive while there is still registered work. Libraries provide this machinery so application code can focus on protocol handling, timeouts, and business decisions.

The important mental model is cooperative scheduling. The loop can only run the next thing when the current callback returns or suspends through a supported async abstraction. PHP does not automatically interrupt a long callback after a few milliseconds. A CPU-heavy loop, blocking file read, synchronous HTTP request, or ordinary sleep() can hold the whole process.

Blocking Versus Non-Blocking

A blocking operation makes the current process wait until the operation finishes:

PHP example
<?php

declare(strict_types=1);

echo 'Before blocking work' . PHP_EOL;
sleep(1);
echo 'After blocking work' . PHP_EOL;

// Prints:
// Before blocking work
// After blocking work

In a one-off CLI command, that may be acceptable. In an event loop, it means every other timer, socket, and pending connection waits too. A WebSocket ping might be delayed, a timeout might fire late, and a server might stop accepting new connections until the blocking call returns.

Non-blocking code starts work and gives control back to the loop while the result is pending. The loop can then run other ready work. Later, when the socket, timer, DNS lookup, process, or HTTP response is ready, the corresponding callback, promise, future, coroutine, or fiber continuation runs.

The whole stack has to cooperate. Calling a non-blocking HTTP client from a callback can fit the model. Calling a traditional synchronous client inside the same callback blocks the loop. Putting blocking code inside a different function name does not change the runtime behavior.

ReactPHP And Amp In The Ecosystem

ReactPHP is an event-driven PHP ecosystem built around components such as an event loop, promises, streams, sockets, DNS, child processes, and HTTP clients and servers. Its official event-loop documentation describes a common loop interface, static Loop accessors, timers, future ticks, signals, read streams, and write streams. ReactPHP is often used for long-running CLI processes, socket servers, daemons, concurrent HTTP clients, and tools that need evented I/O without adopting a full application server.

Amp is another async PHP ecosystem. Modern Amp uses fibers to let async code read more like sequential PHP while still coordinating work through an event loop and awaitable operations. It includes libraries for concurrency, cancellation, synchronization, sockets, HTTP, and process handling.

A beginner does not need to memorize every component in both ecosystems. The useful distinction is architectural. ReactPHP commonly exposes callbacks, promises, streams, and loop primitives directly. Amp often hides more coordination behind fiber-aware APIs. Both require non-blocking-compatible libraries and both run inside long-lived PHP processes.

Do not mix the ecosystems casually in production code. Some packages interoperate, but each runtime has conventions for cancellation, timers, error propagation, and loop ownership. Pick the ecosystem that matches the libraries you need, the team’s debugging ability, and the process model you can operate.

Official references:

Where Event Loops Appear

Event-loop PHP is useful when one process must manage many waiting operations:

  • WebSocket servers and chat systems
  • live dashboards and notification fan-out
  • concurrent HTTP clients for many upstream calls
  • local development servers and protocol tools
  • daemons that watch sockets, timers, files, or child processes
  • queue workers that coordinate many slow I/O tasks
  • bridges between message brokers, webhooks, and internal services

The common pattern is I/O-bound concurrency. The program spends much of its time waiting for network, timer, or stream readiness. Event loops can reduce overhead because the process does not need one thread or process per pending operation.

Event loops are not a free answer to CPU-bound work. If the task is image processing, encryption, PDF generation, compression, or a heavy report calculation, one PHP process still has one CPU thread of execution unless the work is moved to another process, extension, service, or worker pool. An event loop can coordinate CPU workers, but it does not make one callback run on several cores.

One Loop Owner

A program should have a clear loop owner. In a small script, the entry point may configure all listeners and then run the loop. In a framework or package, the application server or runtime may own the loop and user code registers work with it.

Confusion starts when a library tries to run its own loop inside a callback that is already running on another loop. Nested loop ownership can produce deadlocks, unexpected exits, late timers, and hard-to-debug shutdown behavior. Prefer libraries that accept the current loop, return promises or awaitables, or document how they integrate with the selected runtime.

ReactPHP’s documentation recommends attaching work to the same loop instance and running the loop once for many applications. That rule is a good default beyond ReactPHP: configure the process, attach listeners, then let one runtime coordinate readiness.

Timers, Signals, And Shutdown

Timers are not background threads. A timer callback runs only when the loop gets a chance to execute it. If another callback blocks for five seconds, a one-second timer can fire late. Timeouts are still essential, but they are not a substitute for keeping callbacks short and non-blocking.

Long-running processes also need graceful shutdown. A deployment may send a signal. A container may receive a termination request. The process should stop accepting new work, close or drain connections where appropriate, finish or cancel in-flight operations according to policy, flush logs, and exit before the platform kills it.

Signal handling is platform-specific and may require extensions or runtime support. Do not assume a signal example works the same on every operating system. Document the supported deployment environment and test shutdown in the same container or service manager used in production.

State And Memory

Because the process remains alive, memory and state remain alive too. This is the same risk seen in application servers and long-running workers, but event-loop code often adds more listeners, closures, buffers, and streams.

Avoid storing request-specific values in global variables, static properties, or singleton services. Remove listeners when connections close. Cancel timers that no longer have a purpose. Put upper bounds on buffers and queues. Watch memory over time, not only at startup.

A slow memory leak may not show up in a five-minute test. Run soak tests that keep connections open, reconnect clients, trigger failures, and measure memory after many iterations. Treat uncontrolled growth as a release blocker because an event-loop process is expected to stay alive.

Failure Handling

Failures must be attached to the async operation that can fail. A rejected promise, failed future, closed stream, connection reset, DNS failure, timeout, or uncaught exception should not disappear into the loop. Log the operation identity, remote peer, timeout budget, retry decision, and correlation ID where available.

Do not retry everything automatically. Reconnecting a WebSocket server, retrying a webhook delivery, and resubmitting a payment command have different safety rules. The event loop gives you a place to schedule retries; it does not decide whether the retry is safe.

Use backoff and limits. A tight reconnect loop can overwhelm the same dependency that is already failing. A timer that retries every millisecond is a production incident waiting to happen.

When PHP-FPM Is Simpler

For many websites and CRUD applications, PHP-FPM remains the simpler default. Each request has a familiar lifecycle. Blocking database calls are normal. Memory is reset more often. Existing hosting, monitoring, debugging, and framework assumptions are usually built around request isolation.

Choose event-loop PHP when the product has a real concurrency or protocol reason: many open sockets, many concurrent outbound requests, interactive long-lived connections, or a daemon that mostly waits on I/O. Do not choose it merely because it sounds modern. The operational cost is real, and the team must be ready to test long-running state, cancellation, memory, shutdown, and library compatibility.

What To Check

Before moving on, make sure you can:

  • explain the difference between a short PHP-FPM request and a long-running event-loop process
  • describe why blocking code delays unrelated work inside the same loop
  • identify ReactPHP and Amp as async PHP ecosystems rather than language features
  • choose one loop owner for a process
  • explain why timers can fire late when callbacks block
  • list the state, memory, shutdown, and failure risks of long-running loops
  • decide when ordinary PHP-FPM plus queues is simpler than event-loop PHP

After this lesson, you should be able to review a PHP process and say whether an event loop is part of the design, what work the loop coordinates, which libraries must be non-blocking, and what operational checks are required before trusting the process in production.

Practice

Task: Explain Blocking In An Event Loop

Create a small PHP example or review note that explains why blocking work is dangerous inside an event loop.

Requirements

  • Include a tiny queue or callback example that runs multiple tasks.
  • Show where a blocking call would delay later work.
  • Explain the difference between this simple example and a real event-loop library.
  • Name one situation where ReactPHP or Amp could be useful.
  • Name one situation where PHP-FPM would be simpler.

Check your work

The answer should make the event-loop tradeoff understandable without requiring a full async framework installation.

Show solution
PHP example
<?php

declare(strict_types=1);

$tasks = [
    static function (): string {
        return 'read socket';
    },
    static function (): string {
        sleep(1);
        return 'slow blocking task';
    },
    static function (): string {
        return 'run timer callback';
    },
];

foreach ($tasks as $task) {
    echo $task() . PHP_EOL;
}

// Prints:
// read socket
// slow blocking task
// run timer callback

The sleep(1) blocks the process, so the timer callback cannot run until the slow task finishes. A real event-loop library such as ReactPHP or Amp uses non-blocking I/O and scheduling primitives instead of this simple array loop.

ReactPHP or Amp can be useful for a long-running process that manages many sockets or many concurrent HTTP requests. PHP-FPM is simpler for a standard CRUD application where each browser request can be handled independently and long work can be moved to a queue.

Task: Classify Event-Loop Safety

Review the following operations for a PHP process that uses an event loop to manage WebSocket clients and periodic upstream API checks.

Operations

  1. Reading a small configuration array already loaded at startup.
  2. Calling a synchronous HTTP client from inside a message callback.
  3. Scheduling a timer to send heartbeat pings.
  4. Running a CPU-heavy report calculation inside the same callback that handles socket reads.
  5. Registering a close listener that removes a client connection from an in-memory connection map.

Requirements

For each operation, classify it as safe, risky, or unsafe for the event-loop callback path. Explain the reason and name a safer design for any risky or unsafe item.

Check your work

The answer should distinguish blocking I/O, CPU-bound work, short in-memory work, timers, and listener cleanup.

Show solution

Reading a small configuration array loaded at startup is safe if it is ordinary in-memory data and does not perform hidden file or network reads. The callback should still avoid mutating shared configuration with request-specific values.

Calling a synchronous HTTP client inside a message callback is unsafe. While that client waits for DNS, connection, TLS, headers, and body data, the loop cannot process other clients. Use a non-blocking HTTP client from the selected async ecosystem, move the call to a separate worker, or redesign the callback to enqueue the work and return quickly.

Scheduling a heartbeat timer is safe when the timer callback is short and failures are handled. The timer should be cancelled when the connection closes. It should not perform long blocking work itself.

Running a CPU-heavy report calculation in the socket callback is unsafe for the event loop. It can hold the process even though no I/O is waiting. Move the calculation to a queue worker, child process, separate service, or bounded worker pool, then notify the client when the result is ready.

Registering a close listener that removes the connection from a map is safe and necessary cleanup. Without it, the process can leak memory and keep trying to write to closed connections. The cleanup callback should be defensive and idempotent because close events and error paths can overlap.

To verify the reasoning, ask whether each operation returns control to the loop quickly. Safe event-loop callbacks are short, bounded, and compatible with the runtime's non-blocking model.

Task: Plan A Small Event-Loop Daemon

A team wants a PHP daemon that watches a message stream, calls two internal HTTP APIs for each message, and publishes a combined notification to connected dashboard clients.

Write a short design note for the daemon.

Requirements

Include:

  • which parts need non-blocking libraries
  • who owns the event loop
  • how timeouts and retries are handled
  • how connection and timer cleanup works
  • how shutdown should behave during deployment
  • one reason this might be better than PHP-FPM
  • one reason a normal queue worker might still be simpler

Check your work

The design should make the runtime tradeoff explicit. It should not assume that ordinary blocking HTTP clients become safe because the process uses an event loop.

Show solution

The daemon should have one entry point that creates or obtains the selected runtime's event loop, registers the message-stream listener, registers dashboard connection handlers, configures timers, and then lets the runtime run the loop. Libraries used for the message stream, HTTP calls, dashboard sockets, and timers must be compatible with that loop or runtime.

Each message should start two non-blocking HTTP requests with explicit timeout budgets. Temporary failures can be retried with bounded backoff when the notification is safe to delay. Validation errors, authorization failures, or malformed upstream responses should be logged and handled without retry loops. The log entry should include the message ID and a correlation ID.

Dashboard connection cleanup should remove closed sockets from the connection map and cancel any per-connection timers. Global timers should be cancelled during shutdown. Buffers and pending messages need limits so a slow dashboard client cannot make the process grow without bound.

During deployment, the daemon should stop accepting new dashboard connections and new stream messages, finish or cancel in-flight HTTP requests according to the product policy, flush logs, close sockets, and exit before the process manager sends a hard kill. This shutdown path should be tested in the same container or service manager used in production.

This design can be better than PHP-FPM when many dashboard clients stay connected and the process mostly waits for I/O. A normal queue worker may still be simpler if dashboard delivery can be handled through polling or a separate managed service, because ordinary workers are easier to debug and can use blocking clients safely.