Architecture Overview

OxPHP is a single-binary HTTP server that replaces the traditional nginx + PHP-FPM stack. It handles HTTP parsing, TLS termination, routing, PHP execution, compression, and observability in one process with no external dependencies at runtime.

How OxPHP works

OxPHP combines two runtime layers in a single process:

  1. Async HTTP layer. An event-driven network layer accepts TCP connections, performs TLS handshakes, parses HTTP requests, and sends responses. It handles thousands of concurrent connections using non-blocking I/O, so one slow client never blocks another.
  2. PHP worker pool. A pool of dedicated PHP workers executes your PHP scripts. In standard mode, each worker handles one request at a time. In Worker mode with fiber multiplexing enabled, a single worker can serve multiple concurrent requests: when a script calls oxphp_sleep() or oxphp_async_await(), the fiber yields the thread and the worker switches to the next request.
  3. Async pool (optional). Separate OS threads for tasks submitted via oxphp_async(). Enabled by setting ASYNC_WORKERS > 0. Isolated from the worker pool so background tasks do not block HTTP request handling.

The two layers communicate through a bounded queue. When an HTTP request arrives that needs PHP execution, the async layer places it in the queue. An available PHP worker picks it up, executes the script, and returns the response to the async layer for delivery to the client.

This separation means that network I/O (accepting connections, reading headers, compressing responses, serving static files) never competes with PHP execution for resources. Each layer scales independently.

Worker pool

The PHP worker pool determines how many PHP scripts can execute concurrently. OxPHP supports two pool modes.

Static pool

A fixed number of workers start at boot and remain running for the lifetime of the server. This is the default mode.

bash
PHP_WORKERS=8 # exactly 8 workers PHP_WORKERS=0 # auto-detect (default): half of available CPU cores, minimum 1

Dynamic pool

Workers scale up and down based on demand. Specify a minimum and maximum count separated by a colon:

bash
PHP_WORKERS=2:16 # start with 2, scale up to 16 under load

When all current workers are busy, OxPHP spawns new workers up to the maximum. When a worker has been idle for longer than PHP_WORKERS_IDLE_SECONDS (default: 30 seconds), it is retired back down toward the minimum.

Queue and backpressure

Between the async HTTP layer and the worker pool sits a bounded queue. Its capacity defaults to the initial worker count multiplied by 128 and can be overridden with QUEUE_CAPACITY. For a static pool, the initial count is the configured worker count. For a dynamic pool (MIN:MAX), the initial count is the minimum.

When the queue is full (all workers are busy and the queue has reached capacity), OxPHP immediately returns a 529 Site is Overloaded response to the client with a Retry-After header. Status code 529 (non-standard, used by Cloudflare and others) clearly distinguishes overload from application errors (500) and maintenance (503), so alerts and load balancers are easier to configure.

Request flow

Every request passes through the same pipeline, regardless of whether it serves a static file or executes PHP:

graph TD
  Client(["Client"]) --> TLS["TLS termination<br/>(if configured)"]
  TLS --> Parse["HTTP parsing + Request ID"]
  Parse --> Proxy["Trusted proxy resolution<br/>(if TRUSTED_PROXIES set)"]
  Proxy --> Rate["Rate limiting check"]
  Rate --> Route{"Route resolution"}
  Route -->|Static file| Cache["File cache / disk read"]
  Cache --> Compress["Compression + Response headers"]
  Route -->|PHP request| Queue["Bounded queue<br/>(529 if full)"]
  Queue --> Worker["PHP worker executes script"]
  Worker --> Normal["Normal response"]
  Worker --> SSE["SSE streaming (chunked)"]
  Worker --> Early["Early response (finish_request)<br/>+ background work"]
  Compress --> Deliver(["Response to client"])
  Normal --> Deliver
  SSE --> Deliver
  Early --> Deliver
  1. TLS termination. If TLS_CERT and TLS_KEY are configured, OxPHP handles TLS directly. No separate reverse proxy is needed.
  2. HTTP parsing and request ID. The request is parsed and a unique request ID is generated (or an incoming X-Request-ID header is preserved).
  3. Trusted proxy resolution. If TRUSTED_PROXIES is set and the connecting IP is trusted, OxPHP extracts the real client IP, protocol, and host from Forwarded (RFC 7239) or X-Forwarded-* headers. The resolved IP is used for all subsequent steps including rate limiting and access logging. See Trusted Proxies.
  4. Rate limiting. If RATE_LIMIT is set, the client's IP is checked against the per-IP request counter. Requests that exceed the limit receive a 429 Too Many Requests response immediately.
  5. Route resolution. The URL is matched against the configured routing mode (traditional, framework, or SPA). The result is either a static file, a PHP script, or a 404. Worker mode, if enabled, changes how PHP executes the resolved script but does not change route resolution itself.
  6. Static files. Served directly from an in-memory cache (for frequently accessed files) or streamed from disk. OxPHP adds ETag, Last-Modified, and Cache-Control headers automatically.
  7. PHP execution. The request is placed in the bounded queue and picked up by an available worker. If the queue is full, the client receives 529 immediately.
  8. Compression. Text-based responses are compressed with Brotli before being sent when the client sends Accept-Encoding: br (configurable via COMPRESSION_LEVEL).
  9. SSE streaming. If the script sets Content-Type: text/event-stream or calls oxphp_stream_flush(), OxPHP switches to streaming mode: each flush() call sends a chunk to the client immediately without buffering the entire response. In Worker mode, SSE works cooperatively with fiber multiplexing.
  10. Early response. Calling oxphp_finish_request() sends the HTTP response to the client immediately. The script continues executing in the background (writing logs, updating caches, sending notifications) without holding the connection open.
  11. Response delivery. The completed response is sent back over the connection, and if access logging is enabled, a log entry is written.

Worker Mode vs Standard Mode

OxPHP supports two PHP execution models:

Standard mode (default)

Creates a fresh PHP environment for every request. Autoloaders, configuration, and database connections are initialized on each request and torn down afterward. This model is compatible with all PHP applications out of the box.

Worker mode

Keeps PHP processes alive across requests. Your application bootstraps once (loading the autoloader, configuration, and establishing database connections) and then enters a request loop. Between requests, OxPHP automatically resets superglobals, output buffers, and response headers while preserving the bootstrapped state.

Worker mode eliminates per-request startup overhead, which can reduce response times significantly for framework-based applications (Laravel, Symfony, etc.) where bootstrapping is expensive.

To enable worker mode, set WORKER_MODE_ENABLED=true and point ENTRY_FILE at a PHP script that calls oxphp_worker():

php
<?php require __DIR__ . '/../vendor/autoload.php'; $app = new MyApp\Application(); oxphp_worker(function () use ($app) { $app->handle(); });

For a detailed guide, see Worker Mode.

Internal server

If the INTERNAL_ADDR variable is set, OxPHP starts a separate HTTP server on the specified port. It serves three endpoints:

Endpoint Description
GET /health Health status in JSON (uptime, request counters, connections, worker state). Returns 200 during normal operation, 503 during degradation.
GET /metrics Prometheus-format metrics — request counters, response time, queue wait time, worker statistics, compression savings.
GET /config Snapshot of the active configuration in JSON. TLS file paths are redacted.

The internal server does not go through the PHP worker pool or the bounded queue. It responds directly from the async HTTP layer, so it remains accessible even when the PHP pool is fully loaded. This makes /health suitable for Kubernetes liveness/readiness probes.

For details, see Internal Server.

Safety

OxPHP provides several guarantees to keep your application running reliably in production:

  • Request isolation. If a PHP script crashes or triggers a fatal error, only that single request is affected. The server continues handling all other requests normally. The crashed worker is automatically replaced with a fresh one.
  • Automatic worker respawn. OxPHP monitors the health of all PHP workers. If a worker dies unexpectedly, a new worker is started in its place without manual intervention.
  • Backpressure protection. The bounded request queue prevents overload. When the server is at capacity, new requests receive a 529 response with a Retry-After header rather than queueing indefinitely and causing cascading timeouts.
  • Path traversal protection. All URL paths are sanitized before filesystem access. Percent-encoded traversal attempts, .. segments, and paths that escape the document root are blocked.
  • Graceful shutdown. On SIGTERM or SIGINT (Ctrl+C), OxPHP stops accepting new connections and waits for in-flight requests to complete (up to a configurable drain timeout) before exiting.

See also