Prometheus Metrics
OxPHP exposes Prometheus-compatible metrics in text exposition format at GET /metrics on the internal server. They cover request throughput, response times, connection state, worker pool health, static file caching, compression efficiency, and worker mode performance.
Enabling metrics
Set INTERNAL_ADDR to start the internal server:
INTERNAL_ADDR=127.0.0.1:9090Then scrape from Prometheus or any compatible collector:
curl http://localhost:9090/metricsServer metrics
| Metric | Type | Description |
|---|---|---|
oxphp_uptime_seconds |
gauge | Seconds since the server process started |
oxphp_requests_total |
counter | Total HTTP requests received on the main port |
Request metrics
| Metric | Type | Description |
|---|---|---|
oxphp_requests_by_method_total |
counter | Requests by HTTP method. Label: method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, QUERY, OTHER) |
oxphp_responses_by_status_total |
counter | Responses by status class. Label: status (1xx, 2xx, 3xx, 4xx, 5xx) |
oxphp_request_bytes_total |
counter | Total request body bytes received |
oxphp_response_bytes_total |
counter | Total response body bytes sent |
oxphp_request_cancelled_total |
counter | Cancelled requests by reason. Label: reason (client_abort, timeout, shutdown). Always emitted |
Only methods and status classes with at least one recorded event are emitted. Zero-count labels are omitted.
Request duration histogram
| Metric | Type | Description |
|---|---|---|
oxphp_request_duration_us |
histogram | End-to-end request duration in microseconds for all requests (static files and PHP) |
Bucket boundaries (microseconds): 100, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000, +Inf.
Use this histogram to track overall latency, identify slow endpoints, and measure tail latency percentiles.
Connection metrics
| Metric | Type | Description |
|---|---|---|
oxphp_active_connections |
gauge | Currently open TCP connections on the main port |
oxphp_pending_requests |
gauge | Requests currently dispatched to PHP workers (queued and in-flight) |
oxphp_dropped_requests_total |
counter | Requests where the PHP worker failed after accepting the request |
Worker pool metrics
| Metric | Type | Description |
|---|---|---|
oxphp_workers_current |
gauge | Current number of PHP worker threads |
oxphp_workers_min |
gauge | Minimum worker count (equals current count in static mode) |
oxphp_workers_max |
gauge | Maximum worker count (equals current count in static mode) |
oxphp_workers_idle |
gauge | Workers not currently processing a request |
oxphp_busy_workers |
gauge | Workers currently processing a request |
oxphp_workers_spawned_total |
counter | Total workers spawned since startup (includes initial workers) |
oxphp_workers_retired_total |
counter | Total workers retired due to idle timeout (dynamic mode only) |
Worker supervisor metrics
Per-worker observability emitted by the worker supervisor. Each series carries a worker_id label (slot index). These appear once the supervisor is tracking per-worker state.
| Metric | Type | Description |
|---|---|---|
oxphp_worker_request_age_seconds |
gauge | Age of the in-flight request on each worker, in seconds. Label: worker_id |
oxphp_worker_long_running_total |
counter | Supervisor scans that observed a request older than the stuck threshold. Label: worker_id |
oxphp_worker_stuck_total |
counter | Stuck-classification counter per worker. Labels: worker_id, kind (io, c_call, cpu) |
Queue wait histogram
| Metric | Type | Description |
|---|---|---|
oxphp_queue_wait_us |
histogram | Time a request waits in the queue before a worker picks it up, in microseconds |
Bucket boundaries (microseconds): 50, 100, 250, 500, 1000, 2500, 5000, 10000, 50000, +Inf.
High queue wait times indicate that all workers are busy and you should increase PHP_WORKERS or QUEUE_CAPACITY.
Rate limiting metrics
| Metric | Type | Description |
|---|---|---|
oxphp_rate_limited_total |
counter | Requests rejected by the rate limiter (returned 429) |
oxphp_php_deny_total |
counter | Requests blocked by PHP_DENY_PATHS (.php execution denied). See PHP Execution Deny-List |
Static file cache metrics
| Metric | Type | Description |
|---|---|---|
oxphp_static_cache_hits_total |
counter | Static file requests served from the in-memory cache |
oxphp_static_cache_misses_total |
counter | Static file requests that required a disk read |
Compression metrics
| Metric | Type | Description |
|---|---|---|
oxphp_compressed_responses_total |
counter | Responses compressed with Brotli |
oxphp_compression_bytes_saved_total |
counter | Total bytes saved by compression (original size minus compressed size) |
Worker mode metrics
These metrics are only emitted when worker mode is active (WORKER_MODE_ENABLED=true).
Global counters
| Metric | Type | Description |
|---|---|---|
oxphp_worker_mode_enabled |
gauge | Always 1 when worker mode is active |
oxphp_worker_requests_handled_total |
counter | Total requests processed by persistent workers |
oxphp_worker_recycles_total |
counter | Total worker recycles (worker exited and was respawned) |
oxphp_worker_recycles_by_reason_total |
counter | Recycles by reason. Label: reason (scheduled, max_memory, error) |
oxphp_worker_soft_resets_total |
counter | Total soft resets performed between requests |
Per-worker gauges
| Metric | Type | Description |
|---|---|---|
oxphp_worker_memory_bytes |
gauge | Current PHP heap usage per worker. Label: worker (slot index, e.g., "0", "1") |
oxphp_worker_uptime_seconds |
gauge | Seconds since each worker was spawned. Label: worker |
oxphp_worker_requests_count |
gauge | Requests handled by each worker instance. Label: worker |
Worker request duration histogram
| Metric | Type | Description |
|---|---|---|
oxphp_worker_request_duration_us |
histogram | PHP handler execution time per request in microseconds (worker mode only) |
Bucket boundaries (microseconds): 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, +Inf.
This histogram measures time spent inside the PHP handler callback, excluding queue wait time. Use it to identify slow handlers and track tail latency in worker mode.
Async pool metrics
These metrics require ASYNC_WORKERS set to a non-zero value, and each has its own emission gate: the counters appear only after at least one task has been dispatched or rejected, the _in_flight / _in_flight_limit gauges appear once the pool has wired its in-flight counter, and oxphp_async_output_discarded_bytes_total appears only after some output has been discarded.
| Metric | Type | Description |
|---|---|---|
oxphp_async_tasks_dispatched_total |
counter | Total async tasks dispatched to the background pool |
oxphp_async_tasks_completed_total |
counter | Async tasks that completed successfully |
oxphp_async_tasks_failed_total |
counter | Async tasks that threw an exception |
oxphp_async_tasks_cancelled_total |
counter | Async tasks that were cancelled |
oxphp_async_tasks_rejected_total |
counter | Async tasks rejected at dispatch — because the pool queue was full or the in-flight cap (ASYNC_MAX_FIBERS × ASYNC_WORKERS) was reached |
oxphp_async_tasks_stranded_total |
counter | Workers left running past an await_race / await_any timeout. Each stranded task can extend RSHUTDOWN by up to 5 seconds. |
oxphp_async_tasks_in_flight |
gauge | Async tasks currently queued or running (emitted once the pool wires its in-flight counter) |
oxphp_async_tasks_in_flight_limit |
gauge | Maximum concurrent async tasks (ASYNC_MAX_FIBERS × ASYNC_WORKERS) |
oxphp_async_output_discarded_bytes_total |
counter | Bytes of async-task output discarded at worker idle (an echo in an async task has no client to receive it) |
Grafana dashboard tips
The following PromQL queries are useful for building dashboards:
Request rate (requests per second):
rate(oxphp_requests_total[5m])Average response time (milliseconds):
rate(oxphp_request_duration_us_sum[5m])
/ rate(oxphp_requests_total[5m]) / 1000p99 request duration (milliseconds):
histogram_quantile(0.99, rate(oxphp_request_duration_us_bucket[5m])) / 1000Error rate (5xx responses as a percentage):
rate(oxphp_responses_by_status_total{status="5xx"}[5m])
/ rate(oxphp_requests_total[5m]) * 100Worker pool utilization:
oxphp_busy_workers / oxphp_workers_currentQueue saturation (drop rate per second):
rate(oxphp_dropped_requests_total[5m])p99 queue wait (microseconds):
histogram_quantile(0.99, rate(oxphp_queue_wait_us_bucket[5m]))Static file cache hit rate:
rate(oxphp_static_cache_hits_total[5m])
/ (rate(oxphp_static_cache_hits_total[5m]) + rate(oxphp_static_cache_misses_total[5m]))Bytes saved by compression per second:
rate(oxphp_compression_bytes_saved_total[5m])Worker mode p99 latency (microseconds):
histogram_quantile(0.99, rate(oxphp_worker_request_duration_us_bucket[5m]))Worker recycle rate (per minute):
rate(oxphp_worker_recycles_total[5m]) * 60Average worker memory usage:
avg(oxphp_worker_memory_bytes)Prometheus scrape config
Add a scrape job to your prometheus.yml:
scrape_configs:
- job_name: "oxphp"
scrape_interval: 15s
static_configs:
- targets: ["oxphp:9090"]For Kubernetes service discovery:
scrape_configs:
- job_name: "oxphp"
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
regex: oxphp
action: keep
- source_labels: [__meta_kubernetes_pod_ip]
target_label: __address__
replacement: "$1:9090"See also
- Health Checks — the
/healthand/configendpoints on the internal server - Configuration Reference — all environment variables including
INTERNAL_ADDR - Graceful Shutdown — how connection draining affects
oxphp_active_connections - Worker Mode — persistent workers and the metrics they emit