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 AH00161 with 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

CauseWhat it looks likeFirst thing to check
Slow backend (proxy/PHP-FPM)Scoreboard dominated by W states, backend latency up, 504s then 503s, Apache CPU and memory normalScoreboard state distribution; direct curl to the backend bypassing Apache
Keepalive hoarding (prefork/worker)Many K states holding workers, low RPS but high BusyWorkersScoreboard K count; KeepAliveTimeout value
Slow clients / SlowlorisMany R (reading) states, low bytes transferred, backend healthyScoreboard R count; source IP concentration via ss
Log stallMany L (logging) states, throughput collapseddf -h on the log filesystem
Graceful restart pile-upMany G states, process count above MaxRequestWorkers, memory elevatedError log for repeated “resuming normal operations”
Traffic genuinely exceeds capacityMostly W with normal per-request latency, backend healthy, RPS at record highsPeak 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: 0 with BusyWorkers at the configured MaxRequestWorkers confirms active saturation. MaxRequestWorkers is not exposed in server-status output; you need the value from your config.
  • Scoreboard characters: _ waiting, R reading, W sending reply, K keepalive, D DNS, L logging, G graceful finish, . open slot. W is ambiguous: actively writing bytes, waiting on a backend, and internal processing all show as W.
  • On event MPM, K states in the scoreboard should be rare because keepalives live on the listener thread (tracked as ConnsAsyncKeepAlive). Significant K on event MPM is abnormal. On prefork/worker, K is the classic worker hoarder.
  • Brief non-zero Recv-Q during bursts is normal. Sustained Recv-Q > 0 alongside 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

  1. 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.
  2. Snapshot the scoreboard. The state distribution is the single most diagnostic view Apache exposes. It tells you where workers are stuck: W heavy means downstream or response generation, R heavy means slow clients, K heavy means keepalive hoarding, L heavy means logging, G heavy means restart pile-up.
  3. 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).
  4. Check the listen backlog. ss -ltn Recv-Q tells you whether connections are currently queueing. This is the leading indicator that appears before user-visible failures.
  5. 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.
  6. 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

SignalWhy it mattersWarning sign
AH00484 in error logDefinitive confirmation the pool saturatedAny occurrence
BusyWorkers / MaxRequestWorkersPrimary saturation gauge; cliff-edge at 100%Sustained above 80%; above 95% with IdleWorkers 0
Scoreboard state distributionShows what is holding workersW >50% sustained; R >20%; significant K on event MPM
Listen backlog Recv-QLeading indicator before refused connectionsSustained non-zero; approaching ListenBacklog (511)
5xx rate (503 specifically)User-visible impact of exhaustionAny sustained 503s
Backend response timeThe most common root causeP95 above 2x baseline
Per-child RSSSets the safe ceiling for MaxRequestWorkersMaxRequestWorkers 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 ThreadsPerChild must be at least MaxRequestWorkers; for prefork, ServerLimit must be at least MaxRequestWorkers. Raising ServerLimit needs a full restart, and setting it far higher than needed allocates unused shared memory.
  • ThreadsPerChild multiple: for event/worker, set MaxRequestWorkers to a multiple of ThreadsPerChild or Apache rounds down and logs a warning.
  • systemd limits: TasksMax and MemoryMax in 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 LimitNOFILE before 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 Accesses delta, 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.