You found this line in the Apache error log:
AH00484: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting
This is Apache explicitly telling you that every worker slot in its pool was occupied and it had nowhere to put a new connection. From that moment, new connections queue in the kernel’s TCP listen backlog, and once the backlog fills, clients get connection refused. Users experience timeouts or a dead site while the Apache process looks perfectly healthy in ps.
The message appears once when the limit is hit, not once per rejected request. A single occurrence during a burst that recovers immediately may have caused no user-visible impact, because the listen backlog (default ListenBacklog 511) absorbed the overflow. But it always means you ran out of a finite resource, and the suggested fix in the message text is often the wrong fix. In most production incidents the workers are not too few; they are being held too long by something downstream. Raising the limit without understanding why workers are held trades a queueing problem for an OOM problem.
This article walks through confirming the saturation, identifying what is holding the workers, and fixing the actual cause.
What this means
Apache maps incoming connections to a bounded pool of workers. The shape of that pool depends on the MPM:
- prefork: one process per connection. Each busy connection costs a full process (10-50MB+ RSS depending on loaded modules). The prefork equivalent of this message is logged as
AH00161with identical message text. - worker: processes with multiple threads; each thread handles one connection.
- event (default in 2.4): like worker, but keepalive connections are parked on a dedicated listener thread, so idle keepalives do not consume worker threads.
MaxRequestWorkers caps the total number of simultaneous request-handling workers across all child processes. It was renamed from MaxClients in Apache 2.3.13, so older configs and blog posts use the old name. Defaults: 256 for prefork; ServerLimit (16) times ThreadsPerChild (25) = 400 for event and worker. ServerLimit is a hard ceiling on child processes, so for event/worker it silently constrains how high MaxRequestWorkers can effectively go, and raising ServerLimit requires a full restart, not a graceful reload.
The failure sequence when the pool saturates:
flowchart TD A[Workers held: slow backend, slow clients, keepalive hoarding] --> B[BusyWorkers reaches MaxRequestWorkers] B --> C[AH00484 logged once] C --> D[New connections queue in listen backlog] D --> E[Recv-Q grows toward ListenBacklog 511] E --> F[Backlog full: connections refused] F --> G[LB health checks time out, server pulled from rotation]
There is no graceful degradation on this path. The transition from “serving normally” to “every new connection queues” is nearly instantaneous, which is why AH00484 incidents feel like a cliff.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Slow backend (proxy/PHP-FPM) | Scoreboard dominated by W states, backend latency up, 504s then 503s, Apache CPU and memory normal | Scoreboard state distribution; direct curl to the backend bypassing Apache |
| Keepalive hoarding (prefork/worker) | Many K states holding workers, low RPS but high BusyWorkers | Scoreboard K count; KeepAliveTimeout value |
| Slow clients / Slowloris | Many R (reading) states, low bytes transferred, backend healthy | Scoreboard R count; source IP concentration via ss |
| Log stall | Many L (logging) states, throughput collapsed | df -h on the log filesystem |
| Graceful restart pile-up | Many G states, process count above MaxRequestWorkers, memory elevated | Error log for repeated “resuming normal operations” |
| Traffic genuinely exceeds capacity | Mostly W with normal per-request latency, backend healthy, RPS at record highs | Peak BusyWorkers trend vs. MaxRequestWorkers over days |
The slow backend case is the most common in proxy deployments by a wide margin. Workers fill from the backend side, not from traffic overload: each proxied request holds a worker while it waits, so a backend that goes from 50ms to 5s holds each worker 100x longer at the same request rate.
Quick checks
All read-only and safe to run during an incident. Paths shown for Debian; on RHEL use /var/log/httpd/error_log and httpd instead of apache2.
# 1. Confirm AH00484 occurrences and timing
grep "AH00484" /var/log/apache2/error.log | tail -20
# 2. Worker utilization right now (requires mod_status)
curl -s "http://localhost/server-status?auto" | grep -E "BusyWorkers|IdleWorkers"
# 3. Scoreboard state distribution: what are workers actually doing
curl -s "http://localhost/server-status?auto" | grep "Scoreboard:" | \
awk '{print $2}' | fold -w1 | sort | uniq -c | sort -nr
# 4. Listen backlog depth (Recv-Q = queued, Send-Q = max)
ss -ltn | grep -E ':80\s|:443\s'
# 5. 5xx rate over recent requests
tail -1000 /var/log/apache2/access.log | \
awk '$9 ~ /^5/ {e++} END {print "5xx rate:", (e+0)/NR*100 "%"}'
# 6. Bypass Apache and test the backend directly (adjust host/port/path)
curl -s -o /dev/null -w "TTFB: %{time_starttransfer}s Total: %{time_total}s\n" \
--max-time 10 http://backend-host:8080/health
# 7. Per-child memory: the number that decides whether you can raise the limit
ps -C apache2 -o rss --no-headers 2>/dev/null || ps -C httpd -o rss --no-headers
Interpretation notes:
IdleWorkers: 0withBusyWorkersat the configuredMaxRequestWorkersconfirms active saturation.MaxRequestWorkersis not exposed in server-status output; you need the value from your config.- Scoreboard characters:
_waiting,Rreading,Wsending reply,Kkeepalive,DDNS,Llogging,Ggraceful finish,.open slot.Wis ambiguous: actively writing bytes, waiting on a backend, and internal processing all show asW. - On event MPM,
Kstates in the scoreboard should be rare because keepalives live on the listener thread (tracked asConnsAsyncKeepAlive). SignificantKon event MPM is abnormal. On prefork/worker,Kis the classic worker hoarder. - Brief non-zero
Recv-Qduring bursts is normal. SustainedRecv-Q > 0alongside full workers means the kernel is buffering connections Apache cannot accept. If the counter approaches the backlog limit, drops are imminent.
How to diagnose it
- Confirm the event and its window.
grep "AH00484"shows when saturation started and whether it recurs. Correlate the timestamp with 5xx in the access log and with your monitoring. - Snapshot the scoreboard. The state distribution is the single most diagnostic view Apache exposes. It tells you where workers are stuck:
Wheavy means downstream or response generation,Rheavy means slow clients,Kheavy means keepalive hoarding,Lheavy means logging,Gheavy means restart pile-up. - If proxying, test the backend directly. Compare backend TTFB against its baseline. If the backend is slow or hung, Apache is a victim, not the cause. Check the error log for proxy timeout messages (504 territory) versus pool exhaustion (503).
- Check the listen backlog.
ss -ltnRecv-Q tells you whether connections are currently queueing. This is the leading indicator that appears before user-visible failures. - Rule out resource ceilings. Check per-child RSS and total Apache memory against RAM, and per-child FD counts against limits. If memory is already tight, the worker pool cannot safely grow, which confirms the fix must be elsewhere.
- Decide: held workers or genuine overload. If per-request latency for completed requests is normal and the backend is healthy, traffic may genuinely exceed capacity, and a careful limit raise is legitimate. If latency is elevated and the backend is slow, the limit is not your problem.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
| AH00484 in error log | Definitive confirmation the pool saturated | Any occurrence |
| BusyWorkers / MaxRequestWorkers | Primary saturation gauge; cliff-edge at 100% | Sustained above 80%; above 95% with IdleWorkers 0 |
| Scoreboard state distribution | Shows what is holding workers | W >50% sustained; R >20%; significant K on event MPM |
| Listen backlog Recv-Q | Leading indicator before refused connections | Sustained non-zero; approaching ListenBacklog (511) |
| 5xx rate (503 specifically) | User-visible impact of exhaustion | Any sustained 503s |
| Backend response time | The most common root cause | P95 above 2x baseline |
| Per-child RSS | Sets the safe ceiling for MaxRequestWorkers | MaxRequestWorkers x avg RSS approaching 70% of RAM |
Fixes
Fix the slow backend first
If the scoreboard is W-heavy and the backend is slow, no Apache tuning fixes this. Take the instance out of load balancer rotation if the backend is down, or address the backend directly (database lock contention, GC pause, dead dependency). As a temporary pressure valve, reducing ProxyTimeout makes Apache fail fast and return 503 quickly instead of holding workers for the full timeout. That sheds load rather than absorbing it, and it is a stopgap, not a fix.
If you proxy to PHP-FPM, check its pool saturation too (pm.max_children): an exhausted PHP-FPM pool queues requests that hold Apache workers.
Reduce keepalive hoarding (prefork/worker)
If K states dominate and RPS is modest, workers are being parked on idle keepalive connections. Lower KeepAliveTimeout (defaults and common values in the 5-60s range are too generous for prefork under connection-heavy load), or reduce/disable keepalive for the affected vhosts. The structural fix is moving to event MPM, where keepalives are handled by the listener thread and do not consume workers. On event MPM this cause largely disappears.
Block slow-read abuse
If R states dominate with low bytes transferred, confirm source IP concentration:
# Identify connection-heavy source IPs (adjust :80 to :443 as needed)
ss -tn 'sport = :80' | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20
Block offending sources at the firewall, not in Apache config, because Apache workers are already exhausted. Ensure mod_reqtimeout is active with something like RequestReadTimeout header=20-40,MinRate=500 body=20,MinRate=500 so slow senders are disconnected before they accumulate.
Clear log stalls and restart pile-ups
L-dominated scoreboard: check df -h on the log filesystem and free space, then restart httpd after the disk is fixed. G-dominated: reduce graceful restart frequency and set GracefulShutdownTimeout (for example 30s) so old-generation children cannot linger indefinitely.
Raise MaxRequestWorkers safely (only when traffic genuinely exceeds capacity)
This is legitimate only when workers are busy doing real work at normal per-request latency. The constraint is memory:
safe_max_workers = (usable RAM for Apache) / (average per-worker RSS)
For prefork, per-worker cost is a full process RSS. For event/worker, memory is amortized across threads but still scales with active processes. Keep Apache’s worst-case footprint (MaxRequestWorkers times observed per-worker RSS, for prefork) under roughly 70% of RAM. Also check the other ceilings that silently override your intent:
- ServerLimit: for event/worker,
ServerLimit x ThreadsPerChildmust be at leastMaxRequestWorkers; for prefork,ServerLimitmust be at leastMaxRequestWorkers. RaisingServerLimitneeds a full restart, and setting it far higher than needed allocates unused shared memory. - ThreadsPerChild multiple: for event/worker, set
MaxRequestWorkersto a multiple ofThreadsPerChildor Apache rounds down and logs a warning. - systemd limits:
TasksMaxandMemoryMaxin the unit file cap Apache regardless of its config. On cgroup v2 systems, exceeding the memory limit triggers the OOM killer inside the cgroup, which can kill individual Apache children and produce confusing partial failures. - File descriptors: each connection and each backend socket costs an FD. Check per-child FD counts against
LimitNOFILEbefore scaling workers up.
Set MaxConnectionsPerChild to a finite value (commonly 5000-10000) so children recycle periodically; this bounds memory leaks from mod_php and similar modules that would otherwise inflate per-worker RSS until your memory math no longer holds.
Prevention
- Alert on the corroborating signals, not just the log line. Page when BusyWorkers/MaxRequestWorkers exceeds 0.95 with IdleWorkers at 0 for more than 2 minutes AND at least one of: Recv-Q sustained above 0, AH00484 logged, or 503s appearing. Gate on server uptime over 600s to suppress cold-start noise.
- Track peak BusyWorkers as a capacity trend. If daily peaks drift toward 80% of MaxRequestWorkers, you have runway to plan before AH00484 fires. Headroom target: at least 25% idle at the highest normal traffic period.
- Monitor backend latency separately from Apache latency. In proxy deployments, “Apache down” and “backend down” look identical from outside. Backend P95 above 2x baseline is your earliest warning of a worker-exhaustion cascade.
- Baseline the scoreboard. Most teams never look at it. A time series of state distribution turns “why is Apache slow” into a one-glance answer.
- Know your MPM. Interpretation of keepalive signals, per-worker memory cost, and safe limits all depend on it. Event is the default in 2.4 and the right choice for most production deployments.
How Netdata helps
Netdata’s Apache collector polls server-status and turns the point-in-time snapshots into the correlations this incident needs:
- BusyWorkers and IdleWorkers over time, so you see saturation building at daily peaks instead of discovering it from the AH00484 log line.
- Scoreboard-adjacent throughput signals (requests per second from the
Total Accessesdelta, bytes served) so you can distinguish “workers busy doing work” from “workers stuck waiting”: rising BusyWorkers with falling completed RPS is the slow-backend signature. - Per-second resolution that catches the cliff-edge transition, which minute-resolution polling typically misses.
- ML-based anomaly detection on worker utilization and throughput, which flags the deviation from your normal daily pattern before the pool hits 100%.
- Correlation with system metrics on the same dashboard: per-process memory, CPU, and network, so you can check in one view whether there is headroom to raise the limit or whether memory is already the binding constraint.
Netdata’s Apache HTTP Server monitoring with Netdata brings these signals together with per-second metrics and ML anomaly detection.
Related guides
- Apache 503 Service Unavailable: worker exhaustion versus proxy pool exhaustion
- Apache BusyWorkers and IdleWorkers: reading worker utilization from mod_status
- Apache scoreboard states explained: what _ S R W K D C L G tell you
- Apache MaxRequestWorkers tuning: sizing the worker pool against memory
- Apache HTTPD monitoring checklist: the signals every production web server needs
- How Apache HTTPD actually works in production: a mental model for operators
- Apache HTTPD monitoring maturity model: from survival to expert
- Apache listen queue overflow: Recv-Q growth, ListenBacklog, and refused connections
- Apache keepalive consuming workers: KeepAliveTimeout, the K state, and MPM choice
- Apache ListenBacklog vs net.core.somaxconn: the silently truncated accept queue
- Apache CLOSE_WAIT and TIME_WAIT: connection leaks versus normal churn
- Apache 502 Bad Gateway: a backend that returned an invalid response






