The listen queue is the kernel-managed socket backlog between the web server and PHP-FPM workers. When it carries a sustained non-zero depth, requests are arriving faster than workers can drain them. By the time the web server logs a 502, the queue has already overflowed and the kernel has started dropping connections.
The signal is easy to miss for two reasons. First, the FPM status page field is a point-in-time snapshot: a 10-second poller can report 0 even while bursts spill into the queue and drain between polls. Second, on Unix domain sockets (the most common production transport), the status page always reports 0 for all three queue fields due to a long-standing PHP bug. You have to read the kernel’s Recv-Q via ss, or poll at 1-second intervals, to see the real depth.
What this means
The listen queue (listen queue on the FPM status page, Recv-Q on the LISTEN line of ss) is the number of FastCGI connections parked in the kernel socket backlog, each waiting for an idle worker. Every connection in the queue is a real user request that has already been forwarded to PHP-FPM and is on hold.
A sustained non-zero value means the pool cannot keep up with current demand. Each queued request accumulates latency equal to its queue wait on top of normal processing time. When the queue depth reaches listen.backlog, the kernel stops accepting new connections and silently drops them. From the web server’s perspective that looks like ECONNREFUSED or a connect timeout, surfacing to users as 502 Bad Gateway.
By the time you see 502s in the web server logs, the queue has already overflowed. The queue depth itself is the early warning, often minutes ahead of actual failures.
flowchart LR
A[Web server receives request] --> B[Connects to FPM socket]
B --> C{Idle worker available?}
C -- yes --> D[Worker handles request]
C -- no --> E[Connection enters listen queue]
E --> F{Queue full at listen.backlog?}
F -- yes --> G[Kernel drops connection]
G --> H[Web server logs 502]
F -- no --> I[Waits for worker]
I --> DCommon causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Worker pool exhaustion | active processes at pm.max_children, idle processes at 0, queue climbing | FPM status page active/idle ratio |
| Slow dependency contagion | Low CPU, queue growing, slow log entries on DB or external API | FPM slow log stack traces |
| Traffic spike beyond provisioned capacity | accepted conn rate 3x baseline, queue grows in step | Web server request rate vs FPM accepted conn rate |
pm.max_children set too low for traffic | max children reached counter incrementing, queue forms | Compare peak active workers to pm.max_children |
ondemand cold-start latency | Brief queue spike on first request after idle, clears in seconds | Pool pm mode in config |
| Backlog too small for burst pattern | Recv-Q hits Send-Q on ss, 502s appear, kernel ListenOverflows increment | ss -lxn Send-Q vs net.core.somaxconn |
Quick checks
All commands below are read-only and safe on a production host.
# FPM status page queue fields (adjust path to match pm.status_path)
curl -s http://127.0.0.1/fpm-status | grep "^listen queue\|^max listen queue\|^listen queue len\|^active processes\|^idle processes\|^max children reached"
# Kernel-level queue depth on the FPM listening socket (Unix socket)
# Recv-Q = current backlog depth, Send-Q = configured backlog
ss -lxn | grep php
# TCP transport variant (adjust port)
ss -tlnp | grep 9000
# Kernel counters for connections dropped because the backlog was full
nstat -az | grep -iE "ListenOverflows|ListenDrops"
# Effective somaxconn (caps listen.backlog)
cat /proc/sys/net/core/somaxconn
# Pool config: confirm pm mode, max_children, listen.backlog
# Path varies by distribution; this covers Debian/Ubuntu
grep -E "^(pm\.|listen\.)" /etc/php/*/fpm/pool.d/*.conf
# Full per-worker view: which scripts are running and for how long
curl -s 'http://127.0.0.1/fpm-status?full' | grep -E "request duration|request URI|state"
On Unix domain sockets, ss is the most reliable signal because the status page reports 0 for queue depth (see diagnosis step 2 below).
How to diagnose it
Confirm the queue is actually growing, not a single noisy sample. Poll the status page or
ssat 1-second intervals for at least 30 seconds. A single non-zero reading may be a transient burst that drains on its own. A sustained or climbing value is real saturation.Verify the signal source. If PHP-FPM listens on a Unix domain socket, the status page fields
listen queue,max listen queue, andlisten queue lenalways report 0 due to a PHP bug in the status code (it usestcp_info, which only works for INET sockets). Usess -lxn | grep phpand readRecv-Qinstead. For TCP, both the status page andsswork.Check whether workers are all busy. If
active processesequalspm.max_childrenandidle processesis 0, the pool is the bottleneck. If active is low but the queue grows anyway, the master is not dispatching connections to workers, which usually points to socket permission issues or a master process problem.Distinguish demand from slow requests. If
accepted connrate has spiked, the cause is demand. Ifaccepted connrate is normal but active workers are pinned high, requests are taking longer than usual and tying up workers.Read the slow log. If
request_slowlog_timeoutis set, the slow log shows where workers are blocked: PDO calls, curl_exec, session_start, file I/O. If the slow log is empty, the directive is probably 0 (disabled), which is the most common monitoring gap.Check kernel drop counters.
nstat -az | grep -i listen. Non-zeroListenOverflowsmeans connections have already been dropped at the backlog. Users have already seen failures.Inspect per-worker request URIs. The
?fullstatus output shows what each active worker is executing. A single endpoint dominating the active set points to a slow or hot path in the application.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
listen queue (status page) or Recv-Q (ss) | Pending requests waiting for a worker | Any sustained non-zero value during normal traffic |
max listen queue (status page high-water mark) | Reveals past saturation even when current depth is 0 | Non-zero value you were not aware of |
listen queue len (status page) | Configured backlog depth; effective cap is min(listen.backlog, net.core.somaxconn) | max listen queue approaching this value |
active processes / pm.max_children | Pool utilization; 1.0 means the next request queues | Sustained above 0.85 during normal traffic |
idle processes | Headroom buffer; near 0 means one burst from queuing | Sustained near 0 in dynamic or static mode |
max children reached counter | Process manager blocked from spawning (dynamic/ondemand) | Incrementing during normal traffic |
accepted conn rate | Throughput entering the pool | Sudden drop while web server traffic holds steady |
slow requests counter | Application-level slowness, often the upstream cause | Rate increasing |
Kernel ListenOverflows / ListenDrops | Connections already dropped at the backlog | Any non-zero increment |
| Web server 502/504 rate | User-visible failures | Any sustained non-zero rate |
Fixes
Either give the pool more capacity (more workers, or faster workers), or reduce the demand arriving at it.
Worker pool exhaustion
If active processes is at pm.max_children and the queue is growing, the pool is undersized.
- Raise
pm.max_childrenif memory headroom exists. Estimate the safe ceiling first:(available_RAM - OS_overhead) / avg_worker_PSS. Use PSS, not RSS, because forked workers share opcache pages. Raisingpm.max_childrenwithout checking memory leads to OOM. - Reduce per-request duration if workers are I/O-bound. The slow log tells you where they are stuck.
- Consider
staticmode if bursts outrun the dynamic scaler. Dynamic PM forks workers based on spare-server thresholds, which introduces fork latency under sudden bursts. Static mode keeps all workers resident at the cost of constant memory.
Apply changes with systemctl reload php-fpm (SIGUSR2 to the master). Note that the service name varies by distribution (php-fpm, php8.2-fpm, etc.).
Slow dependency contagion
The most common trigger. Workers that normally complete in 50ms now block for seconds on a database, external API, or NFS mount. They hold their slot the entire time, so a few slow requests can drain the whole pool.
- Fix the dependency. The slow log stack trace shows which call is blocked. Check the database slow query log, external API latency, or NFS stats.
- Set
request_terminate_timeout(for example, 30s) so a single stuck request cannot hold a worker indefinitely. - Add circuit breakers or shorter timeouts in application code. Default PHP cURL timeouts are often very high.
- Do not just raise
pm.max_children. New workers hit the same slow dependency and also block. The queue drains briefly, then refills.
Traffic spike
If accepted conn rate has spiked well above baseline and the queue grows in step, demand has outstripped capacity.
- Enable rate limiting at the web server (nginx
limit_req, Apachemod_qos) to protect FPM from burst traffic it cannot absorb. - Scale horizontally if this is sustained growth rather than a transient burst.
- Pre-warm opcache after any restart so cold-start compilation does not compound the spike.
Backlog too small
If Recv-Q is hitting Send-Q on ss, or max listen queue approaches listen queue len, connections are being dropped at the kernel.
- Raise
listen.backlogin the pool config. The effective backlog is capped bynet.core.somaxconn.
- Check
net.core.somaxconnbecause it caps the effective backlog:cat /proc/sys/net/core/somaxconn. Raise it if your configuredlisten.backlogexceeds the current value. - Restart, do not reload. Changing
listen.backlogrequires a fullsystemctl restart php-fpmbecause the listening socket must be recreated. SIGUSR2 reload will not pick up the new value.
A larger backlog buys time but does not fix the underlying capacity gap. It is a buffer, not a cure.
Prevention
- Poll at 1-second intervals. The status page is a point-in-time snapshot. A 10-second poller will miss transient queue build-ups entirely.
- Monitor the kernel socket, not just the status page. On Unix domain sockets, the status page queue fields are always 0. Read
ssRecv-Q for the actual depth. - Track
max listen queuebetween restarts. It is a high-water mark that reveals past saturation even when current depth is 0. It resets on restart, so frequent deploys erase this history. - Alert on any sustained non-zero queue depth during normal traffic hours, not just when it approaches
listen.backlog. By then, connections are already being dropped. - Track kernel
ListenOverflowsandListenDropsas the definitive signal that connections have been refused at the kernel. - Keep 20-30% of workers idle during peak. Below that, one slow dependency or one burst pushes you into the queue.
- Restrict the status page to localhost or trusted IPs. The full status output exposes request URIs and script paths.
How Netdata helps
- Per-second collection of
listen queue,max listen queue,active processes,idle processes, andmax children reachedfrom the FPM status page, so transient queue build-ups are visible rather than averaged away by a 10-second poller. - Kernel socket metrics from
ssand/proc/net/netstat(ListenOverflows,ListenDrops) alongside the FPM status fields, which is the only reliable way to see drops on a Unix domain socket where the status page reports 0. - Anomaly detection on queue depth and worker utilization, surfacing a slow climb before it crosses a static threshold.
- Correlation across the request path: FPM queue depth against web server 502/504 rates, accepted conn rate, slow request counter, and per-worker request duration. A queue growing while accepted conn rate holds steady tells a different story than a queue growing alongside a traffic spike.
- Per-pool dashboards for multi-pool deployments, because a single saturated pool behind a round-robin load balancer means every Nth request is slow.






