skip to content

In Apache httpd running mpm_event, how do MaxRequestWorkers, ServerLimit and ThreadsPerChild relate, and what happens if you set MaxRequestWorkers higher than ServerLimit multiplied by ThreadsPerChild?

level: middleimportance: must knowfreq 60%

answer

  1. concurrency is a product, not a single dial
  2. threads only exist inside children
  3. the parent will not exceed one hard cap
  4. a startup warning, not a startup failure
  5. the config says one number, the server means another

basics

~20 s

MaxRequestWorkers is the total worker threads across all children, and it cannot exceed ServerLimit times ThreadsPerChild. Set it higher and httpd logs a startup warning and silently lowers it to that product, so your intended concurrency never takes effect.

solid answer

~50 s

Under `mpm_event` (and `mpm_worker`) concurrency is a product. `ThreadsPerChild` sets how many request-handling threads live in one child, `ServerLimit` is the hard ceiling on how many children the parent may ever create, and `MaxRequestWorkers` is the total number of worker threads that may be busy at once across all of them. The parent only spawns children up to `MaxRequestWorkers / ThreadsPerChild`, so `MaxRequestWorkers` above `ServerLimit × ThreadsPerChild` is unreachable: httpd logs a warning at startup saying it exceeds the ServerLimit value and is being decreased, then runs with the product instead. The trap is that this is a warning, not a failure — the server starts, and everyone believes it is sized for the number in the config. `StartServers` and the `MinSpareThreads`/`MaxSpareThreads` window only control how quickly you ramp toward that ceiling. Note also that `ServerLimit` and `ThreadLimit` are re-read only on a full restart, not a graceful reload.

code

apacheconf · 8 lines
apacheconf
<IfModule mpm_event_module>
    StartServers             4
    MinSpareThreads         75
    MaxSpareThreads        250
    ThreadsPerChild         25
    ServerLimit             16
    MaxRequestWorkers      400
</IfModule>

go deeper

for a junior

Know that MaxRequestWorkers caps how many requests Apache serves at once, and that under a threaded MPM it counts threads rather than processes.

for a middle

Be able to state the relationship as arithmetic — ServerLimit times ThreadsPerChild — and describe the startup warning that silently lowers an over-large MaxRequestWorkers.

for a senior

Show that you verify the effective ceiling from the error log rather than the config file, and that you know ServerLimit and ThreadLimit need a full restart.

for a principal

Own the sizing rule for a fleet: derive the ceiling from memory per worker and downstream capacity, then keep the three directives internally consistent so nobody inherits a config that means something other than it says.

## The four numbers Under a threaded MPM, Apache's concurrency is not one dial but a small system of them: - `ThreadsPerChild` — request-handling threads inside each child process. Fixed for the life of a child. - `ServerLimit` — the hard upper bound on the number of child processes the parent may ever create. - `ThreadLimit` — the hard upper bound on what `ThreadsPerChild` may be set to. - `MaxRequestWorkers` — the total number of worker threads that may be serving requests at once, across all children. This is the number that actually caps simultaneous request processing. ```apacheconf <IfModule mpm_event_module> StartServers 4 MinSpareThreads 75 MaxSpareThreads 250 ThreadsPerChild 25 ServerLimit 16 MaxRequestWorkers 400 MaxConnectionsPerChild 0 </IfModule> ``` Here `16 × 25 = 400`, so `MaxRequestWorkers 400` is exactly reachable. That consistency is the point of the block. ## What goes wrong when the arithmetic does not hold Set `MaxRequestWorkers 1000` while leaving `ServerLimit 16` and `ThreadsPerChild 25` and httpd does not refuse to start. It writes a warning to the error log along the lines of *MaxRequestWorkers of 1000 exceeds ServerLimit value of 16 servers, decreasing MaxRequestWorkers to 400 — to increase, please see the ServerLimit directive*, and then serves traffic with an effective ceiling of 400. The configuration file says 1000, the running server means 400, and unless someone reads the log at startup nobody notices until the day traffic reaches 400 and requests begin queueing. The reason is structural rather than arbitrary: worker threads only exist inside children, the parent will not create more children than `ServerLimit`, and each child has exactly `ThreadsPerChild` of them. `ServerLimit × ThreadsPerChild` is therefore the physical maximum number of worker threads that can exist, and `MaxRequestWorkers` cannot exceed a quantity that cannot be built. The same shape applies to `ThreadLimit`: raising `ThreadsPerChild` above `ThreadLimit` is capped in the same way. And under `mpm_prefork` the product collapses, because a child has one thread — there `MaxRequestWorkers` simply cannot exceed `ServerLimit`, and the two are usually set to the same value. ## Why the limits exist separately at all `ServerLimit` and `ThreadLimit` exist because they size shared structures the parent allocates once at startup — most visibly the scoreboard that tracks every worker slot. That is why they can only be changed by a full stop and start: a graceful restart keeps the parent process alive and cannot resize what it already allocated. Raising them far beyond what you need is not free, since the parent reserves for the maximum. So the intended discipline is: choose the concurrency you actually want, set `MaxRequestWorkers` to it, then set `ServerLimit` to `MaxRequestWorkers / ThreadsPerChild` rounded up — not to some very large number so that you never have to think about it. ## Which number should move When you decide you need more concurrency, you have two ways to get it, and they are not equivalent. Raising `ThreadsPerChild` gives each child more threads: fewer processes, less per-process overhead, but a larger blast radius because a crash takes all threads in that child down with it. Raising `ServerLimit` gives you more children: more isolation, more process overhead. Most tuning guidance keeps `ThreadsPerChild` at a moderate value (25 is a common default) and moves `ServerLimit`, so that no single child holds too much of your capacity. `StartServers`, `MinSpareThreads` and `MaxSpareThreads` are a different category entirely — they do not change the ceiling, only how eagerly the parent forks toward it and how much idle capacity it keeps in reserve. A server that is slow to absorb a traffic spike usually needs a higher `MinSpareThreads`, not a higher `MaxRequestWorkers`. ## The name `MaxRequestWorkers` was called `MaxClients` in older versions and the old name still works as an alias, which is why so much surviving documentation and so many copied config snippets use it. If you see `MaxClients` in a modern config, it is the same dial under its old name — and it is a useful signal that the block was copied from advice written for a different era, and possibly for a different MPM.

  • Why can ServerLimit and ThreadLimit only be changed by a full restart?
    They size structures the parent allocates once at startup, most visibly the scoreboard holding one slot per possible worker. A graceful restart keeps the same parent process, so it cannot grow those allocations. Everything else in the MPM block — MaxRequestWorkers within the existing limits, the spare thread window — is re-read on a graceful reload.
  • If you need more concurrency, do you raise ThreadsPerChild or ServerLimit?
    Usually ServerLimit. More threads per child means fewer processes and slightly less overhead, but a crash or a leak takes out a larger share of your capacity at once. Keeping ThreadsPerChild moderate and adding children preserves isolation. Either way, recheck that MaxRequestWorkers still equals the product you intend.
  • What do StartServers and MinSpareThreads change about the ceiling?
    Nothing. They control ramp-up and reserve: how many children exist at startup and how much idle thread capacity the parent keeps available before forking more. They matter for how gracefully you absorb a sudden spike, but the maximum simultaneous requests is still MaxRequestWorkers, bounded by ServerLimit times ThreadsPerChild.

saying these in an interview costs you the question

  • Believes MaxRequestWorkers alone sets concurrency under a threaded MPM
  • Assumes httpd fails to start when the arithmetic is inconsistent
  • Sets ServerLimit enormously high 'to be safe'
  • Thinks a graceful reload applies a new ServerLimit
  • Confuses StartServers with the concurrency ceiling

context