PHP Runtime And Server Environment

ReactPHP Event Loop, Streams, And Promises

Use ReactPHP when the problem is naturally evented or I/O-bound. A chat server, WebSocket gateway, TCP protocol bridge, concurrent HTTP checker, webhook delivery daemon, local development server, or background process that watches many sockets can fit. A normal CRUD request handled by PHP-FPM usually does not need ReactPHP. The extra runtime rules are justified only when the process benefits from staying alive and coordinating many waiting operations.

The official ReactPHP documentation is organized by components. The EventLoop component provides the loop abstraction and timer, signal, read-stream, and write-stream APIs. Promise provides future-result primitives. Stream and Socket provide non-blocking stream and socket building blocks. Async adds utilities for writing asynchronous flows in a more structured style. Check the official documentation for the exact component versions and PHP requirements used by a project because ReactPHP components are versioned separately.

Official references:

The Runtime Shape

A ReactPHP program usually has one process entry point. That entry point loads Composer dependencies, configures the loop or uses ReactPHP's Loop accessor, registers timers, stream listeners, socket servers, or async operations, and lets the loop run until there is no more work or the process is stopped.

A simplified shape looks like this:

require vendor/autoload.php
configure services and limits
register timers, sockets, streams, and signal handlers
start the event loop
callbacks run when resources become ready
shutdown removes listeners, closes streams, and exits

That is a different lifecycle from PHP-FPM. In PHP-FPM, a bad singleton often disappears after a request. In ReactPHP, the same object can survive for hours. That is useful for connection pools, caches, and protocol state, but dangerous for tenant state, user data, stale authorization decisions, and growing buffers.

Loop Ownership

ReactPHP's EventLoop documentation describes using the Loop class as a convenient global accessor and notes that the loop should usually be configured and run as one program-level coordinator. In older code you may see explicit loop creation and dependency injection. Both styles can work, but the important rule is that the process has one clear loop owner.

Do not let random libraries start and stop their own loops inside application callbacks. That can make timers late, prevent other work from running, or exit the process at surprising times. A good ReactPHP-compatible library returns promises, accepts streams, registers callbacks with the current loop, or documents how it integrates.

When writing reusable application services, keep ReactPHP concerns near the runtime boundary. Domain services should not need to know whether the caller is PHP-FPM, a queue worker, or ReactPHP. Put non-blocking clients behind interfaces when the same business rule may run in several contexts.

Timers And Deferred Work

Timers are one of the first ReactPHP features learners meet. A one-off timer schedules a callback for later. A periodic timer repeats until cancelled. A future tick schedules work to run soon after the current callback returns.

A timer is not a thread. It fires when the loop can run it. If another callback blocks for five seconds, a one-second timer can be delayed. For operational code, a timer callback should be short, bounded, and prepared for late execution.

You can model the timer decision without installing ReactPHP:

PHP example
<?php

declare(strict_types=1);

function timerPolicy(string $purpose, bool $cancelWhenConnectionCloses): string
{
    if ($purpose === 'heartbeat' && $cancelWhenConnectionCloses) {
        return 'safe recurring timer';
    }

    if ($purpose === 'retry' && $cancelWhenConnectionCloses) {
        return 'bounded retry timer';
    }

    return 'review lifetime and cancellation';
}

echo timerPolicy('heartbeat', true) . PHP_EOL;
echo timerPolicy('retry', false) . PHP_EOL;

// Prints:
// safe recurring timer
// review lifetime and cancellation

Real ReactPHP code should keep the timer handle so it can cancel the timer. Losing that handle is a common source of memory leaks and unexpected callbacks after a client has disconnected.

Streams And Backpressure

Streams are central to ReactPHP because sockets, process pipes, and many protocol bodies are streams. A readable stream emits data chunks. A writable stream accepts chunks. In non-blocking code, writing is not always immediate. Buffers can fill, remote clients can slow down, and the process must avoid keeping unlimited data in memory.

Backpressure means the producer slows down when the consumer cannot keep up. Without backpressure, a fast upstream can fill memory while a slow client receives data at a few bytes per second. ReactPHP stream components expose events and methods for handling readable data, writable capacity, closure, and errors. The exact API depends on the component, so read the selected component documentation rather than guessing from synchronous stream habits.

Treat every stream as fallible. Remote peers close connections. TLS handshakes fail. Process pipes end. Partial writes happen. A robust daemon has close and error handlers that remove listeners, cancel timers, release per-connection state, and log enough context to diagnose the failure.

Promises And Errors

ReactPHP Promise represents a result that may be available later. A promise can fulfill with a value or reject with a reason. This lets a function start work and return immediately while the loop continues. Later, handlers respond to success or failure.

Promises are useful only if errors are handled. An unobserved rejection can turn into a silent failure, a logged warning, or a process-level problem depending on the library and version. Attach failure handlers close to the boundary where the operation has enough context: which URL, which client, which timeout, which message ID, and what retry policy applies.

A useful review question is: if this upstream call times out, where does that failure go? If the answer is unclear, the ReactPHP code is not production-ready.

Non-Blocking Dependencies

ReactPHP works when the dependencies cooperate with the event loop. Use ReactPHP-compatible HTTP, DNS, socket, stream, and process libraries for work that happens inside the loop. A synchronous database client, blocking HTTP client, or filesystem operation can still freeze the process.

Some blocking work is unavoidable. The practical options are to move it to a separate process, put it behind a queue, use a bounded worker pool, or accept that the event loop is not the right place for that task. Do not hide blocking code behind a promise that immediately performs the blocking call before returning. That gives the caller a promise-shaped API while still blocking the loop.

When reviewing a package, check whether it explicitly supports ReactPHP or returns compatible promises and streams. Also check maintenance, timeout support, cancellation behavior, and how errors are surfaced. A package that performs well in a short CLI command may be unsafe in a daemon.

State, Limits, And Observability

A ReactPHP process needs limits. Set maximum connections, maximum buffer sizes, timeout budgets, retry counts, and queue depths. Decide what happens when a limit is reached: reject new work, close the slow connection, drop a low-priority message, or apply backpressure.

Log bounded operation names rather than arbitrary payloads. Useful metrics include active connections, open streams, pending promises, timer counts where available, memory usage, event-loop lag, reconnect attempts, retry counts, and error categories. Event-loop lag is especially important because it shows when callbacks are blocking or the process is overloaded.

Do not expose unbounded user data as metric labels. A URL path with user IDs, a raw socket address list, or a GraphQL document can explode metric cardinality. Use route names, operation names, component names, and stable error codes.

Shutdown And Deployment

A ReactPHP daemon should handle deployment as deliberately as it handles startup. On shutdown, stop accepting new connections, cancel periodic timers that should not fire again, stop reading from message sources, allow safe in-flight work to finish within a deadline, close streams, flush logs, and exit.

If the process runs in a container, systemd service, supervisor, or platform worker, test the exact signal and timeout behavior. A graceful shutdown path that works on a laptop but not in production is not enough. The process manager may send a termination signal and then kill the process after a fixed grace period.

Restart strategy matters. Long-running processes should be safe to restart during deploys and after memory growth. If clients need reconnection logic, document it. If the process owns in-memory state that cannot be lost, that state probably belongs in durable storage instead.

When ReactPHP Fits

ReactPHP is a good candidate when:

  • the process mostly waits on many sockets, streams, timers, or outbound requests
  • maintained ReactPHP-compatible libraries exist for the required protocols
  • the team can operate long-running PHP processes
  • state can be bounded and cleaned up
  • timeouts, retries, and shutdown behavior can be tested
  • the benefit is clearer than using PHP-FPM plus queues

ReactPHP is a poor default when the application is a normal request-response website, depends heavily on blocking libraries, needs CPU-heavy work in the hot path, or lacks operational ownership for daemons.

A sensible adoption path is a small proof of concept around one real bottleneck. Measure concurrency, memory, latency, event-loop lag, error handling, and shutdown. If the proof of concept cannot be operated confidently, do not expand the runtime choice across the application.

What To Check

Before moving on, make sure you can:

  • explain ReactPHP as a component ecosystem for event-driven PHP
  • identify where the event loop is owned and run
  • explain how timers, streams, sockets, and promises fit together
  • recognize blocking dependencies that can freeze the loop
  • design cleanup for timers, streams, and connection state
  • add limits for buffers, retries, connections, and pending work
  • plan graceful shutdown for a long-running daemon
  • decide whether ReactPHP is simpler or riskier than PHP-FPM plus queues

After this lesson, you should be able to review a proposed ReactPHP process and ask the right production questions: which component owns the loop, which dependencies are truly non-blocking, how failures and backpressure are handled, how memory stays bounded, and how the process exits safely during deployment.

Practice

Task: Map ReactPHP Components
  • accept TCP connections from devices
  • parse line-based messages
  • call an internal HTTP API for some messages
  • retry temporary failures later
  • close idle device connections

Map each responsibility to the kind of ReactPHP component or runtime concept it needs.

Requirements

Mention the event loop, sockets or streams, promises or async flow, timers, timeouts, and cleanup. Also identify one place where a blocking dependency would be unsafe.

Check your work

The answer should describe runtime responsibilities, not just list package names.

Show solution

Line parsing is application logic attached to stream data events. It must handle partial lines because network chunks do not necessarily match message boundaries. Per-connection buffers need size limits so a device cannot send endless data without a newline.

Internal HTTP calls need a non-blocking ReactPHP-compatible HTTP client or async abstraction. A synchronous HTTP client would be unsafe inside the stream callback because it would block all other device connections while waiting for the upstream API.

Temporary failures can be represented with rejected promises or failed async operations. Retry scheduling needs timers with bounded backoff and maximum attempts. Idle connection closure also needs timers, and those timers must be cancelled when the connection closes normally.

Cleanup removes stream listeners, closes the connection, cancels per-connection timers, releases buffers, and records a bounded log event. The design is safe only if every long operation either returns control to the loop or is moved out of the loop.

Task: Review A ReactPHP Daemon Design

The daemon accepts WebSocket clients. Each message handler calls file_get_contents() against an internal HTTP URL, writes the result to all clients, and stores the latest message in a static property. A periodic timer broadcasts memory usage every second. Shutdown is handled by the container restarting the process.

Identify the main risks and propose safer changes.

Requirements

Cover blocking I/O, broadcast backpressure, static state, timer lifetime, observability, and graceful shutdown.

Check your work

The answer should explain why the design can work during a small demo but fail under real load or deployment.

Show solution

file_get_contents() against an HTTP URL is blocking. While it waits, the WebSocket server cannot process other messages, pings, closes, or timers. Replace it with a ReactPHP-compatible non-blocking HTTP client, add timeout budgets, and attach success and failure handlers with message context.

Broadcasting to all clients needs backpressure handling. Some clients will be slow or disconnected. The daemon should track write capacity, apply per-client buffer limits, close clients that cannot keep up, and avoid building one unbounded string for every connected client.

The static property is risky if it stores request-specific or user-specific state. If a latest-message cache is genuinely global, name it that way, bound it, and avoid mixing tenant data. Durable state should go to storage rather than living only in process memory.

The periodic timer is reasonable for diagnostics, but it should be cancelled during shutdown and should not become the only observability. Add metrics for active clients, pending upstream requests, write-buffer pressure, rejected promises, reconnects, and event-loop lag.

Relying only on container restart is not graceful shutdown. The process should stop accepting new connections, close or drain existing connections according to policy, cancel timers, finish or cancel pending upstream calls within a deadline, flush logs, and exit before the container's hard kill.

Task: Design A Concurrent HTTP Checker

Design a small ReactPHP command that checks 200 internal URLs every minute and writes a status summary.

Requirements

Describe:

  • how the loop is started and owned
  • how checks are scheduled
  • how concurrency is limited
  • how timeouts and failures are represented
  • how late checks are handled if one minute passes before the previous run finishes
  • how shutdown works

Check your work

The design should avoid starting 200 blocking requests at once and should not allow overlapping runs to grow without bound.

Show solution

The command entry point should own the loop. It loads configuration, creates the non-blocking HTTP client, registers a periodic timer, registers signal handling where supported, and lets the loop run.

The periodic timer starts a check run every minute only if the previous run has finished or if the design explicitly allows overlap. A safer beginner design skips or delays a run when the previous one is still active, then logs that the schedule is falling behind.

Concurrency should be bounded, for example 20 in-flight requests at a time. Each request needs a timeout and a result record containing URL identity, status, duration, and failure category. Results should use stable service names rather than full secret-bearing URLs in logs or metrics.

Failures are represented through rejected promises or async failures and converted into status entries. Temporary network failures, timeouts, HTTP 5xx, and HTTP 4xx should be categorized separately because they lead to different operational actions.

Shutdown should cancel the periodic timer, stop launching new requests, allow current requests to finish until a short deadline, cancel or close the rest if the client supports it, write the final summary, flush logs, and exit. This prevents overlapping runs and unbounded pending work during deployments.