# PHP-FPM Monitoring with OpenTelemetry: Metrics, Alerts, and Dashboards

> Monitor PHP-FPM performance with OpenTelemetry Collector and php-fpm_exporter. Collect process pool metrics, set up alerting on worker exhaustion, and visualize PHP-FPM health in Uptrace.

PHP-FPM (FastCGI Process Manager) is the standard way to run PHP applications in production, but without proper monitoring you are blind to worker pool exhaustion, rising queue depths, and slow requests. This guide shows you how to collect PHP-FPM metrics with the php-fpm_exporter, ship them through OpenTelemetry Collector, and visualize them in Uptrace.

The data flows through the following pipeline:

1. **PHP-FPM** exposes a status endpoint with pool statistics.
2. **php-fpm_exporter** reads the status endpoint and serves metrics in Prometheus format.
3. **OpenTelemetry Collector** scrapes the exporter and forwards metrics to Uptrace.
4. **Uptrace** stores, visualizes, and alerts on the metrics.

## Why Monitor PHP-FPM?

PHP-FPM manages a pool of worker processes that handle incoming PHP requests. Without visibility into pool utilization, you cannot tell whether your application is dropping requests, running out of workers, or leaking memory until users complain.

Monitoring PHP-FPM helps you:

- **Prevent outages** by catching worker pool exhaustion before requests start queuing.
- **Right-size your pools** by observing idle vs. active process ratios.
- **Detect memory leaks** by tracking per-process memory consumption over time.
- **Find slow endpoints** by correlating slow request counts with application traces.

## Prerequisites

Before you begin, make sure you have the following:

- **PHP-FPM** installed and running (PHP 7.x or 8.x).
- **Access to PHP-FPM pool configuration** (typically `/etc/php/8.x/fpm/pool.d/www.conf`).
- **Docker** (recommended) or a Linux host for running the exporter and collector.
- An **Uptrace account** ([cloud](https://app.uptrace.dev/join) or [self-hosted](/get/hosted/docker)) with a project DSN.

## Step 1: Enable the PHP-FPM Status Endpoint

PHP-FPM exposes pool statistics through a built-in status page, but it is disabled by default. Enable it in your pool configuration file.

Edit your pool config (for example `/etc/php/8.2/fpm/pool.d/www.conf`):

```conf
; Enable the status endpoint
pm.status_path = /status

; Optionally listen on a TCP socket instead of a Unix socket
; so the exporter can reach it from another container
listen = 0.0.0.0:9000
```

Restart PHP-FPM to apply the changes:

```shell
sudo systemctl restart php8.2-fpm
```

Verify the status endpoint is working:

```shell
# Using cgi-fcgi (install with: apt-get install libfcgi-bin)
SCRIPT_NAME=/status \
SCRIPT_FILENAME=/status \
REQUEST_METHOD=GET \
cgi-fcgi -bind -connect 127.0.0.1:9000
```

You should see output containing fields like `active processes`, `idle processes`, and `listen queue`.

## Step 2: Install php-fpm_exporter

[php-fpm_exporter](https://github.com/hipages/php-fpm_exporter) is a Prometheus exporter that reads the PHP-FPM status endpoint and exposes the data as Prometheus metrics on port 9253.

### Docker (recommended)

```shell
docker run -d \
  --name php-fpm-exporter \
  --network host \
  hipages/php-fpm_exporter:2 \
  --phpfpm.scrape-uri tcp://127.0.0.1:9000/status
```

### Binary

Download the latest release from [GitHub](https://github.com/hipages/php-fpm_exporter/releases) and run it directly:

```shell
# Download (adjust version and architecture as needed)
curl -L -o php-fpm_exporter \
  https://github.com/hipages/php-fpm_exporter/releases/download/v2.2.0/php-fpm_exporter_2.2.0_linux_amd64

chmod +x php-fpm_exporter

# Start the exporter as an HTTP server on :9253
./php-fpm_exporter server --phpfpm.scrape-uri tcp://127.0.0.1:9000/status
```

Verify the exporter is working by fetching its metrics endpoint:

```shell
curl http://localhost:9253/metrics | grep phpfpm
```

## Step 3: Configure OpenTelemetry Collector

[OpenTelemetry Collector](/opentelemetry/collector) scrapes the Prometheus endpoint exposed by the exporter and forwards the metrics to Uptrace.

Create a file called `otel-collector-config.yaml`:

```yaml
receivers:
  otlp:
    protocols:
      grpc:
      http:

  prometheus/phpfpm:
    config:
      scrape_configs:
        - job_name: php-fpm
          scrape_interval: 15s
          static_configs:
            - targets: ['php-fpm-exporter:9253']

processors:
  resourcedetection:
    detectors: [env, system]
  resource:
    attributes:
      - key: service.name
        value: php-fpm
        action: upsert
      - key: deployment.environment
        value: production
        action: upsert
  cumulativetodelta:
  batch:
    timeout: 10s

exporters:
  otlp/uptrace:
    endpoint: https://api.uptrace.dev:4317
    headers:
      uptrace-dsn: '<UPTRACE_DSN>'

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/uptrace]
    metrics:
      receivers: [otlp, prometheus/phpfpm]
      processors: [cumulativetodelta, batch, resource, resourcedetection]
      exporters: [otlp/uptrace]
  telemetry:
    logs:
      level: 'info'
```

Replace `<UPTRACE_DSN>` with the DSN from your Uptrace project settings.

## Step 4: Run Everything with Docker Compose

The easiest way to run the full stack is with Docker Compose. Create a `docker-compose.yml`:

```yaml
version: '3.8'

services:
  php-fpm:
    image: php:8.2-fpm
    volumes:
      - ./app:/var/www/html
      - ./php-fpm-pool.conf:/usr/local/etc/php-fpm.d/www.conf
    ports:
      - '9000:9000'

  php-fpm-exporter:
    image: hipages/php-fpm_exporter:2
    command:
      - '--phpfpm.scrape-uri=tcp://php-fpm:9000/status'
    ports:
      - '9253:9253'
    depends_on:
      - php-fpm

  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    command: ['--config=/etc/otel-collector-config.yaml']
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - '4317:4317'
      - '4318:4318'
    depends_on:
      - php-fpm-exporter
```

Create a minimal pool configuration file called `php-fpm-pool.conf`:

```conf
[www]
user = www-data
group = www-data

listen = 0.0.0.0:9000

pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15

; Enable status endpoint
pm.status_path = /status

; Enable slow request logging
request_slowlog_timeout = 5s
slowlog = /proc/self/fd/2
```

Start the stack:

```shell
docker compose up -d
```

## Available Metrics

php-fpm_exporter exposes these key metrics:

<table>
<thead>
  <tr>
    <th>
      Metric
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        phpfpm_up
      </code>
    </td>
    
    <td>
      Gauge
    </td>
    
    <td>
      PHP-FPM pool status (1 = up, 0 = down)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        phpfpm_active_processes
      </code>
    </td>
    
    <td>
      Gauge
    </td>
    
    <td>
      Workers currently processing requests
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        phpfpm_idle_processes
      </code>
    </td>
    
    <td>
      Gauge
    </td>
    
    <td>
      Workers available and waiting for requests
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        phpfpm_total_processes
      </code>
    </td>
    
    <td>
      Gauge
    </td>
    
    <td>
      Total number of worker processes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        phpfpm_max_children_reached
      </code>
    </td>
    
    <td>
      Counter
    </td>
    
    <td>
      Number of times the worker limit was hit
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        phpfpm_slow_requests
      </code>
    </td>
    
    <td>
      Counter
    </td>
    
    <td>
      Requests exceeding the slow threshold
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        phpfpm_accepted_connections
      </code>
    </td>
    
    <td>
      Counter
    </td>
    
    <td>
      Total connections accepted by the pool
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        phpfpm_listen_queue
      </code>
    </td>
    
    <td>
      Gauge
    </td>
    
    <td>
      Requests waiting in the listen queue
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        phpfpm_listen_queue_len
      </code>
    </td>
    
    <td>
      Gauge
    </td>
    
    <td>
      Maximum allowed size of the listen queue
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        phpfpm_max_active_processes
      </code>
    </td>
    
    <td>
      Gauge
    </td>
    
    <td>
      Highest observed active process count
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        phpfpm_max_listen_queue
      </code>
    </td>
    
    <td>
      Gauge
    </td>
    
    <td>
      Highest observed listen queue length
    </td>
  </tr>
</tbody>
</table>

Use these metrics in Uptrace or [Grafana alternatives](/comparisons/grafana-alternatives) to create dashboards and set up alerting.

## Alerting on Key Metrics

After the metrics are flowing into Uptrace, set up alerts for the most critical conditions.

### Worker Pool Exhaustion

Alert when PHP-FPM reaches its maximum number of child processes. This means new requests will be queued or rejected:

```yaml
# Uptrace alert rule
name: PHP-FPM max children reached
metrics:
  - phpfpm_max_children_reached as $max_children
query:
  - delta($max_children) > 0
for: 5m
annotations:
  description: >
    PHP-FPM pool has reached max_children {{$max_children}} times
    in the last 5 minutes. Consider increasing pm.max_children.
```

### Listen Queue Growing

Alert when requests start piling up in the listen queue, which indicates workers cannot keep up:

```yaml
# Uptrace alert rule
name: PHP-FPM listen queue too long
metrics:
  - phpfpm_listen_queue as $queue
query:
  - $queue > 10
for: 2m
annotations:
  description: >
    PHP-FPM listen queue is at {{$queue}}. Requests are waiting
    for available workers. Scale workers or investigate slow requests.
```

### PHP-FPM Down

Alert when the exporter can no longer reach the PHP-FPM status endpoint:

```yaml
# Uptrace alert rule
name: PHP-FPM is down
metrics:
  - phpfpm_up as $up
query:
  - $up == 0
for: 1m
annotations:
  description: PHP-FPM pool is not responding. Check the PHP-FPM service status.
```

## Troubleshooting

### Max Children Reached

If `phpfpm_max_children_reached` counter keeps increasing, all workers are busy and new requests are being queued.

**Fix:** Increase the worker pool size in your PHP-FPM pool configuration:

```conf
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
```

The right value for `pm.max_children` depends on available memory. A rough formula: `(total RAM - RAM used by other services) / average PHP process size`.

### High Memory Usage

If individual PHP worker processes grow over time, you likely have a memory leak in application code.

**Fix:** Recycle workers after a set number of requests:

```conf
pm.max_requests = 500
```

This causes each worker to exit and be replaced after handling 500 requests, releasing any leaked memory.

### Slow Requests

Enable slow request logging to identify endpoints or code paths that are taking too long:

```conf
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow.log
```

Review the slow log to find stack traces of requests that exceeded the threshold.

### Exporter Cannot Connect to PHP-FPM

If the exporter logs show connection errors:

1. **Verify PHP-FPM is listening** on the expected address:
```shell
ss -tlnp | grep 9000
```
2. **Check the status endpoint is enabled** by looking for `pm.status_path` in your pool config.
3. **Check network connectivity** between the exporter and PHP-FPM. If running in Docker, make sure they are on the same network.

### No Metrics in Uptrace

If the collector is running but metrics are not showing up in Uptrace:

1. **Check the exporter is serving metrics:**```shell
curl http://localhost:9253/metrics
```
2. **Check the collector logs** for scrape errors:
```shell
docker logs otel-collector 2>&1 | grep -i error
```
3. **Verify the Uptrace DSN** is correct in the collector config.
4. **Check network connectivity** from the collector to `api.uptrace.dev:4317`.

### Collector Scrape Failures

If you see `server returned HTTP status 403 Forbidden` in the collector logs, the exporter cannot reach the PHP-FPM status endpoint. Double-check `pm.status_path` in your pool config and restart PHP-FPM.

## What is Uptrace?

Uptrace is an [OpenTelemetry APM](/opentelemetry/apm) that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.

![Uptrace Overview](/home/screenshots/apm.png)

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules with notifications, and integrations for most languages and frameworks.

Uptrace can process billions of spans and metrics on a single server and allows you to monitor your applications at 10x lower cost.

In just a few minutes, you can try Uptrace by visiting the [cloud demo](https://play.uptrace.dev/) (no login required) or running it locally with [Docker](/get/hosted/docker). The source code is available on [GitHub](https://github.com/uptrace/uptrace).

## What's Next?

With PHP-FPM metrics flowing into Uptrace, you can build dashboards that show pool utilization at a glance and alerts that fire before your application starts dropping requests.

To extend your PHP observability further, explore these related guides:

- [OpenTelemetry PHP tracing](/get/opentelemetry-php/tracing) - Instrument PHP application code with distributed traces
- [Laravel monitoring](/guides/opentelemetry-laravel) - Monitor Laravel applications with auto-instrumentation
- [Symfony monitoring](/guides/opentelemetry-symfony) - Symfony observability with OpenTelemetry
- [MySQL monitoring](/guides/opentelemetry-mysql) - Track database query performance
- [OpenTelemetry Collector](/opentelemetry/collector) - Learn more about collector configuration and processors
- [Distributed tracing tools](/tools/distributed-tracing-tools) - Compare observability backends
