OxPHP is an asynchronous PHP application server written in Rust —
built for production workloads that demand low latency, high concurrency, and zero-config observability.
Documents: English · Русский · 中文
Quick Start · Why OxPHP · Features · Configuration · Roadmap
Warning
OxPHP is not production-ready yet. The project is under active development — APIs may change, edge cases are still being uncovered, and there is no SLA. It is ready for evaluation, staging environments, and early adopters who want to test it on real workloads and report what breaks. Feedback, bug reports, and benchmarks against your stack are exactly what we need right now — open an issue or start a discussion on GitHub.
Two lines. That's it.
FROM ghcr.io/oxphp/oxphp:0.9.0
COPY --chown=www-data:www-data . /var/www/html/publicNote: By default,
DOCUMENT_ROOTis/var/www/html/public— the snippet above copies your app directly into the document root. For Laravel, Symfony, Slim, or any project that already ships apublic/subdirectory, useCOPY --chown=www-data:www-data . /var/www/htmlinstead: the framework's ownpublic/lines up with the defaultDOCUMENT_ROOT.
docker build -t my-app . && docker run -p 80:80 my-app
curl http://localhost/No nginx config. No PHP-FPM pool tuning. No process manager. Just your app.
See the full Quick Start guide for more details.
OxPHP replaces nginx + PHP-FPM with a single container. The server works out of the box — TLS, Brotli compression, rate limiting, Prometheus metrics, health checks, and structured JSON logs are configured via environment variables.
| nginx + PHP-FPM | FrankenPHP | RoadRunner | OxPHP | |
|---|---|---|---|---|
| Language | C | Go + C | Go | Rust + C |
| HTTP/3 | ✅ | ✅ | ✅ experimental | 🔜 roadmap |
| TLS 1.3 | ✅ | ✅ | ✅ | ✅ (rustls) |
| Persistent worker state | ❌ | ✅ | ✅ | ✅ |
| Backpressure / HTTP 529 | manual | ❌ | ❌ | ✅ built-in |
| Prometheus metrics | plugin | built-in (Caddy admin) | built-in plugin | ✅ built-in |
| Structured JSON logs | via log_format |
✅ | ✅ | ✅ built-in |
| Per-IP rate limiting | built-in | community module | ❌ | ✅ built-in |
| Custom error pages | ✅ (nginx config) | ✅ (Caddyfile) | ❌ | ✅ preloaded at startup |
| HTTP 103 Early Hints | ✅ (v1.29+) | ✅ | ✅ | 🔜 roadmap |
| Memory safety | ❌ (C) | partial (Go + cgo) | ✅ (Go, PHP isolated via IPC) | partial (Rust + C FFI) |
| WebSocket server | ✅ (proxies) | ✅ (Mercure) | ✅ (centrifuge plugin) | ❌ |
| Reverse proxy / upstream | ✅ (full-featured) | ✅ (Caddy) | ✅ | ❌ |
| Native install (non-Docker) | apt/yum/brew/port | brew, static binary | brew, binary | ❌ |
| Platforms (runtime) | Linux/BSD/Win/Mac | Linux/Mac/Win | Linux/Mac/Win | Linux only (glibc/musl) |
| Supported PHP versions | 7.4–8.4 | 8.2–8.4 | 7.4–8.4 | 8.4 and 8.5 |
| License | BSD-2 / PHP License | Apache-2.0 | MIT | AGPL-3.0 |
| Age / production track record | 20+ years | 2+ years | 7+ years | <1 year |
See the full documentation for details.
Formal benchmarks are coming soon. We are working on a reproducible test suite covering req/s, latency (p50/p99), memory usage, and worker throughput under concurrent load.
- Native PHP execution — PHP runs directly inside the server process, in a dedicated thread pool
- Full superglobals support:
$_SERVER,$_GET,$_POST,$_COOKIE,$_FILES,php://input— see Superglobals - HTTP Object API —
oxphp_http_request()returns a typed, lazy-loading request object with built-in JSON body parsing, content-detected MIME types for uploads, and a mutable attributes container for middleware — see HTTP Request API - Worker Runtime API —
OxPHP\Server\Worker::current()exposes per-thread introspection (id,requestCount,startTime,memoryUsage,rss,maxMemoryBytes) and the worker entry point (serve) — see Worker class - Shared OPcache across all workers — one worker compiles a file, every worker uses the cached bytecode — see OPcache and JIT
- PHP extension functions —
oxphp_*()helpers for streaming, early response, async, tracing, and request access — see PHP functions reference - Plugin system with typed event dispatch, priority ordering, and PHP function registration
- Attribute-based decorators — intercept function/method calls via PHP 8+ attributes with zero overhead on undecorated code; supports
TARGET_FUNCTION,TARGET_METHOD,TARGET_CLASS— see Decorators - Crash isolation — a fatal error in one request does not take down the server
- Worker mode — persistent PHP processes that stay alive across requests; autoloaders, service containers, and DB connections are initialized once and reused — see Worker mode
- Fiber multiplexing — each worker handles multiple concurrent requests via PHP 8.4 Fibers;
oxphp_sleep()andoxphp_async_await()yield the current fiber instead of blocking the worker thread — see Fiber multiplexing - Automatic recycling by request count or memory threshold
- Worker health monitoring — crashed workers are automatically detected and restarted
- Early response via
oxphp_finish_request()— send the response and keep running background work — see Early response
See the full Async promises guide.
oxphp_async()/oxphp_async_await()— dispatch closures to a dedicated thread pool for true parallel execution- Portable serialization for
usevariables, arguments, and return values — safe cross-thread binary transfer - Supported types: scalars, strings, arrays (nested). Resources and objects rejected with
E_WARNING - Exception & die() safety — exceptions,
die(), andexit()are caught and re-thrown asOxPHP\Async\AsyncException - Timeout support — per-task timeouts with
OxPHP\Async\TimeoutException oxphp_async_await_all()/oxphp_async_await_race()/oxphp_async_await_any()— batch, race (first settled), and any (first fulfilled, JSPromise.anystyle) primitives
Process-wide concurrent primitives that let PHP workers coordinate mutable state without Redis, Memcached, or APCu. Everything lives in-process — per-op cost is microseconds, not network round-trips. See the full Shared state guide and observability reference.
Shared\Counter— atomic int64 accumulator (get,set,add,compareAndSet) — see CounterShared\Atomic— full lock-free int64 primitive (load,store,swap,compareAndSet,fetchAdd/Sub/And/Or/Xor) with explicit memory ordering — see AtomicShared\Flag— atomic bool withcompareAndSetfor one-shot transitions — see FlagShared\Once— run-once container with reentrancy-safe factory — see OnceShared\Mutex— poisoning mutex over a stored value, with reentrancy and cross-thread deadlock detection — see MutexShared\Channel— bounded MPMC queue, fiber-aware (blocking recv yields the current fiber) — see ChannelShared\Map— concurrent string-keyed store with batchedsetMany/getManyand cycle-checked nested values — see MapShared\Pool— bounded object pool with strict per-thread affinity, idle-timeout eviction, and chaos-reclaim on worker death — see PoolShared\Registry— name-keyed handles (Registry::counter('hits', fn() => ...)) so every worker and every request converges on the same entry without external stores — see Registry- Built-in observability —
oxphp_shared_*Prometheus counters and/__ox_shared/{summary,entries,entry,preview,types,graph}JSON endpoints on the internal port - Refcount + lifecycle safety — handles cannot outlive the registry entry; cycle detector rejects graphs that would leak memory
- When you outgrow it, see Migrating to an external store
- HTTP/1.1 + HTTP/2 on one port — the protocol is auto-detected per connection: prior-knowledge h2c over cleartext, or
h2via ALPN under TLS, with transparent HTTP/1.1 fallback. Flow-control windows are tuned for typical PHP response sizes — see HTTP/2 - TLS 1.3 with ALPN — both HTTP/2 and HTTP/1.1 over TLS — see TLS
- 3 routing modes — Traditional (file mapping + always-on PATH_INFO), Framework (
index.phprewrite withPATH_INFO=$request_uri), SPA (index.htmlfor no-extension paths, hard 404 for missing assets). Each mode mirrors a familiar nginxtry_filesconfiguration — see Routing - SSE streaming via
Content-Type: text/event-streamauto-detection oroxphp_stream_flush()— cooperative with fiber multiplexing — see Server-Sent Events - Configurable timeouts — header read, request, and keep-alive — see Timeouts
- LRU file cache for static files (in-memory ≤1 MB, streaming for larger) — see Static files
- HTTP caching with ETag, Last-Modified, and 304 Not Modified
- Brotli compression for text responses (256 B – 3 MB range) — see Compression
- mimalloc allocator for lower allocation latency under contention
- Configurable HTTP server threads — multi-threaded by default (CPU/2), tunable via
TOKIO_WORKERS
Full guide: Distributed tracing.
- W3C Trace Context — automatic
traceparent/tracestatepropagation,$_SERVER['OXPHP_TRACE_ID']for PHP log correlation - OpenTelemetry — OTLP span export (gRPC/HTTP) with semantic conventions, configurable sampling, batch processing
- APM auto-instrumentation — internal PHP functions across PDO, mysqli, cURL, Redis, Memcached, and file I/O hooked at the engine level; every call becomes a span with zero code changes
#[OxPHP\Apm\Trace]decorator — annotate any function or method with a PHP 8 attribute to create spans automatically- PHP tracing SDK — 10
oxphp_apm_*()functions (start,end,attribute,event,error,status,header,trace,trace_id,span_id) for manual span creation, attributes, events, error recording, and trace context propagation - Prometheus metrics at
/metrics— per-worker, zero dependencies — see Metrics - Health check at
/health— ready for K8s readiness probes — see Health checks - Internal server on a separate port for health, metrics, and runtime config — see Internal server
- Structured error logging — PHP errors appear in the server log with
php_error_type,php_file,php_linefields - JSON access logging with optional
trace_id/span_idfields (levels:all,error, off viaACCESS_LOG) — see Access logging - Request ID generation + pass-through (
X-Request-ID); trace-derived when OTel enabled — see Request IDs
Full guide: Profiling.
- Per-request profile capture — triggered by cookie (
OXPROF), header (X-OxPHP-Profile), query (?__oxprof=), or statistical sampling (PROFILER_SAMPLE_RATE); constant-time token compare - Four export formats — xhprof (for xhgui), speedscope (for speedscope.app), pprof (Go tools / Pyroscope), collapsed (FlameGraph)
- Rich per-span data — wall-time, CPU time, memory (start/end), events, attributes — nanosecond precision throughout
- PHP SDK — 7 functions (
OxPHP\Profile\{start, stop, pause, resume, mark, metric, is_active}) + 7 attributes (4 observer-filter:#[Profile]/#[Exclude]/#[Sample]/#[Tag]; 3 decorators:#[Mark]/#[SlowThreshold]/#[MemoryThreshold]) - Shared tree with APM — both plugins read one
Arc<SpanTree>; no double collection; APM continues to export only explicit spans to OTel while the profiler keeps the full tree - In-memory LRU + disk retention — last
PROFILER_RETENTION_COUNTruns always retrievable, token-bucket rate-limited writes, 5 s atomic-rename background trimmer - HTTP push — ship profiles to xhgui or any collector; 3× exponential backoff (100/200/400 ms) with 5 s wallclock cap; xhgui envelope auto-detect
- Internal HTTP routes at
/__profiler/— 8 endpoints (list / metadata / raw / speedscope redirect / DELETE / config / stats / landing) with optional bearer-token auth and path-traversal validation - Prometheus metrics — 6 counters + 1 gauge (runs, spans, bytes, disk drops, push failures, truncated, in-memory runs) via
/metrics
- Bounded request queue with 529 backpressure when full
- Per-IP rate limiting with
X-RateLimit-*headers and 429 responses — see Rate limiting - Custom error pages — pre-loaded at startup, zero I/O on the hot path — see Error pages
- Graceful shutdown — in-flight requests drain within
DRAIN_TIMEOUT_SECONDSon SIGTERM/SIGINT — see Graceful shutdown - Path traversal protection — symlink escape detection — see Symlink allow paths
- Trusted proxy support — real client IP extraction from
Forwarded(RFC 7239) andX-Forwarded-*headers with CIDR-based trust — see Trusted proxies - Dot-path blocking — returns 404 for hidden files (
.env,.git/) with.well-knownexception (RFC 8615) — see Dot-path blocking - Non-root container execution as www-data (UID 82)
flowchart TD
Client([Client])
HTTP["Async HTTP server<br/>single- or multi-threaded"]
Route{Route dispatch}
Static["Static file<br/>LRU cache"]
Queue[("Bounded queue<br/>529 when full")]
NF["404 Not Found"]
Pool["Async pool<br/>oxphp_async / oxphp_async_await"]
Client --> HTTP
HTTP --> Route
Route -->|static| Static
Route -->|miss| NF
Route -->|PHP| Queue
Queue --> PhpWorkers
PhpWorkers -.-> Pool
Pool --> AsyncWorkers
subgraph PhpWorkers [PHP workers — dedicated OS threads]
direction BT
W1[Worker]
W2[Worker]
W3[Worker]
end
subgraph AsyncWorkers [Async workers — dedicated OS threads]
direction BT
A1[Worker]
A2[Worker]
A3[Worker]
end
- Async HTTP server — multi-threaded by default, tunable via
TOKIO_WORKERS - PHP worker pool — each worker is a dedicated OS thread; a crash in one worker does not affect the others
- Requests wait in a bounded queue between the HTTP server and the PHP workers; the queue returns 529 when full
- Async pool — separate threads for
oxphp_async()tasks, preventing slowdowns in the main worker pool - Worker mode — persistent PHP processes that stay alive between requests; autoloaders and DB connections are shared across all requests handled by that worker
When INTERNAL_ADDR is set, a lightweight HTTP server starts on a separate port:
| Endpoint | Description |
|---|---|
GET /health |
JSON health status (uptime, requests, connections) |
GET /metrics |
Prometheus text format metrics |
GET /config |
JSON runtime configuration (TLS paths redacted; internal_addr and error_pages_dir omitted) |
A port-only INTERNAL_ADDR (e.g. :9090) binds loopback; bind 0.0.0.0:9090 only to expose it. When the listener is reachable off-host without INTERNAL_ALLOW_IPS, the server warns at startup. Access control is by network isolation plus the INTERNAL_ALLOW_IPS CIDR allow-list — there is no bearer token by design, since a token invites exposing the port "because it's protected." Health probes are always reachable so orchestrator liveness/readiness checks never break.
APM depends on OTel and shares its TracerProvider via the plugin service registry. Span collection happens on the PHP worker thread; OTLP export runs off the hot path via tokio::spawn.
flowchart LR
subgraph Tokio1 ["Tokio thread — request start"]
TC["Trace context handler<br/>(priority -95)<br/>generates trace_id / span_id"]
OTR["OtelRequestHandler (-80)<br/>records start_us,<br/>sets X-Request-ID"]
end
subgraph PHP ["PHP worker thread"]
SDK["PHP tracing SDK<br/>oxphp_trace_*()"]
DEC["#[OxPHP\\Apm\\Trace]<br/>decorator"]
HOOKS["APM hooks (≈33 fn)<br/>PDO · mysqli · cURL<br/>Redis · Memcached · file I/O"]
STACK[("SPAN_STACK<br/>thread-local")]
PHPERR["PHP errors"]
end
subgraph Tokio2 ["Tokio thread — request end"]
OTC["OtelCompleteHandler<br/>builds root server span"]
APC["ApmCompleteHandler (-70)<br/>parses child spans JSON,<br/>links to root span"]
end
subgraph Export ["Background export (tokio::spawn)"]
BATCH["BatchSpanProcessor<br/>(shared TracerProvider)"]
OTLP["OTLP exporter<br/>gRPC :4317 / HTTP :4318"]
end
TC --> OTR
OTR --> SDK
OTR --> DEC
OTR --> HOOKS
SDK --> STACK
DEC --> STACK
HOOKS --> STACK
STACK -->|Arc<SpanTree> via profile_tree| APC
PHPERR -->|structured log| APC
OTR --> OTC
OTC --> BATCH
APC --> BATCH
BATCH --> OTLP
- Trace context is generated first (priority
-95) whenTRACE_CONTEXT=true(auto-enabled by OTel). OTel's request handler at-80recordsstart_us; APM's handler runs at-70. - Span collection is thread-local — each PHP worker has its own
SPAN_STACK. APM hooks, the#[Trace]decorator, and theoxphp_trace_*()SDK all push onto the same stack; child spans serialize to JSON at request end. - Shared
TracerProvider— OTel registersotel.provideras a plugin service; APM fetches the sameArc<OnceLock<TracerProvider>>so both plugins export to the same batch processor. - Off-hot-path export — both complete handlers
tokio::spawnOTLP export; the HTTP response is returned to the client before spans are sent. - Provider lifecycle — OTel initializes the
BatchSpanProcessorinon_ready()(after the Tokio runtime starts). On shutdown,force_flush()+shutdown()drain pending spans.
All settings are via environment variables — no config files required.
The essentials — what most deployments need to get a service up:
| Variable | Default | Description |
|---|---|---|
LISTEN_ADDR |
0.0.0.0:80 |
Address and port to bind |
DOCUMENT_ROOT |
/var/www/html/public |
Filesystem path to serve files from |
ENTRY_FILE |
(unset) | Single canonical entry script. Unset = Traditional, *.php = Framework, non-.php = SPA. Resolved against DOCUMENT_ROOT |
WORKER_MODE_ENABLED |
false |
Enable persistent worker mode. Requires ENTRY_FILE to point at a .php script |
INTERNAL_ADDR |
(unset) | Internal server for health/metrics/config. A port-only value like :9090 binds 127.0.0.1; use 0.0.0.0:9090 to expose it off-host |
INTERNAL_ALLOW_IPS |
(unset) | Comma-separated CIDRs allowed to reach /metrics, /config and other internal paths. Health endpoints (/health, /healthz, /readyz, /startupz, …) are always allowed. Empty = allow all. Loopback is not implicit — list 127.0.0.1/32 to keep localhost access |
TLS_CERT |
(unset) | Path to TLS certificate PEM file |
TLS_KEY |
(unset) | Path to TLS private key PEM file |
SUPERGLOBALS_ENABLED |
true |
Populate $_GET, $_POST, $_COOKIE, $_FILES, $_SERVER; set false to rely solely on oxphp_http_request() |
ASYNC_WORKERS |
0 (disabled) |
Dedicated async worker threads for oxphp_async() |
Worker pool, queue, rate limiting, timeouts, TLS tuning, static file caching, compression, access logs, trusted proxies, PHP-execution deny rules, and every plugin-scoped variable live in the consolidated reference — see Configuration for the full table.
Boolean values (case-insensitive, trimmed): truthy =
on/true/1/yes; falsy =off/false/0/no. Any non-empty value outside that set — typos liketure— fails fast at startup with an error naming the variable. An unset variable or empty assignment (FOO=) falls back to the default, so Docker Compose / Kubernetes substitutions likeFOO=${FOO}work cleanly when the host variable is missing.
Plugin-scoped env vars (the OTEL_*, OTEL_APM_*, and SHARED_* families) live in the consolidated configuration reference so there is one source of truth:
- OpenTelemetry (
plugin-otel): Configuration → OpenTelemetry. For the export pipeline end-to-end, see the Distributed tracing guide. - APM (
plugin-apm): Configuration → APM. RequiresOTEL_ENABLED=true. - Shared State (
plugin-shared): Configuration → Shared State. Concept-level walkthrough lives in the Shared state guide.
# Host (without PHP — all tests pass, no PHP execution)
cargo build --release
# Docker (with PHP — full functionality)
docker compose buildDOCUMENT_ROOT=./www/public ./target/release/oxphpItems are not ordered by priority. Presence on this list does not guarantee implementation.
Built and evolved with AI under careful human guidance.
