Pular para o conteúdo
EdgeServers
Blog

apache

Apache MPM event in 2026 — sizing the thread pool we actually run in production

Prefork is a museum piece. Worker is fine. Event is what you want — and most Apache installs we audit have it tuned wrong.

13 de maio de 2026 · 9 min · por Sudhanshu K.

Apache MPM event in 2026 — sizing the thread pool we actually run in production

Every time we onboard a new customer running Apache httpd, the first thing we look at is the MPM configuration. About 70% of the time it's still prefork, usually because the install was bootstrapped years ago when someone needed mod_php and never revisited. Another 20% is worker with default numbers from the distribution package. Maybe 10% is event, and of those, perhaps half are sized appropriately.

This is a post about the third group — getting mpm_event configured correctly — and a quiet argument for why Apache, properly tuned, is still the right tool for a lot of workloads in 2026.

The three MPMs, briefly

If you haven't thought about this in a few years, here's the model:

  • prefork — one process per request, no threads. Forking is expensive, memory footprint is huge, but each request is fully isolated from the others. The reason it survived for so long: mod_php is not thread-safe, so prefork was the only safe option for PHP under Apache.
  • worker — multiple processes, each with multiple threads. Each thread handles one request from accept to response. Much lower memory, much better throughput, but still occupies a thread for the entire connection lifetime (including the time spent waiting for a slow client to drain bytes).
  • event — same process/thread model as worker, but threads hand idle keep-alive connections back to a dedicated listener that handles them with epoll/kqueue. A thread is occupied only while it's actually doing work.

For keep-alive workloads — which is almost everything in 2026 because HTTP/2 demands persistent connections — event is materially better than worker. Worker pins a thread for the duration of every keep-alive idle period; event doesn't. On a busy site with 5,000 concurrent keep-alive connections, that's the difference between needing 5,000 threads and needing maybe 400.

The reason event isn't universal yet is the PHP holdover. If you're using mod_php, you need prefork, full stop. But in 2026 there's essentially no reason to use mod_php — PHP-FPM with mod_proxy_fcgi is faster, more isolated, and lets you run event. We migrate every prefork customer off it during onboarding.

Sizing the thread pool: the numbers that actually matter

Most distributions ship event with conservative defaults that were appropriate for a 2GB VPS in 2014. Here's what we actually deploy on a typical 4-vCPU / 8GB customer host serving a PHP-FPM backend:

# /etc/apache2/mods-available/mpm_event.conf
<IfModule mpm_event_module>
    StartServers             3
    ServerLimit              16
    ThreadLimit              64
    MinSpareThreads          75
    MaxSpareThreads          250
    ThreadsPerChild          64
    MaxRequestWorkers        1024
    MaxConnectionsPerChild   10000
    AsyncRequestWorkerFactor 2
</IfModule>

The reasoning behind each number:

ThreadsPerChild = 64. This is the unit of parallelism inside a single child process. Higher means fewer processes (less memory overhead, less context switching between processes) but a single child crash takes down more connections. 64 is the sweet spot we've landed on after a lot of experimentation; 25 (the Debian default) is too low and 128 starts to show contention on the per-process locks.

ServerLimit = 16, ThreadsPerChild = 64. Together these cap the absolute maximum at 16 * 64 = 1024 threads. That has to be at least MaxRequestWorkers, otherwise the server can't actually reach the configured worker limit.

MaxRequestWorkers = 1024. The single most important number in the file. This is the cap on simultaneous active requests. Too low and Apache queues connections at the TCP layer, latency goes up, eventually requests get dropped. Too high and you exhaust either memory or the PHP-FPM pool downstream.

The right value is dictated by the slowest thing downstream. If PHP-FPM has pm.max_children = 50, having MaxRequestWorkers = 1024 is theatre — Apache will accept 1024 requests but 974 of them will block waiting for a PHP slot. We size MaxRequestWorkers to be roughly 2x the downstream concurrency limit, so there's headroom for static assets, health checks, and brief queuing during PHP saturation, but not so much that we're shipping connection floods to a backend that can't handle them.

MaxConnectionsPerChild = 10000. Recycles each child process after this many connections. Defends against slow memory leaks in third-party modules. Set to 0 (the default) and a single child can run for months, growing in RSS the whole time. 10,000 means each child lives maybe a few hours on a busy site, which is short enough that any leak gets reclaimed before it matters.

AsyncRequestWorkerFactor = 2. This is the lever that makes event do its thing. It allows the listener to hand back up to 2 * MaxRequestWorkers keep-alive connections to async management while only MaxRequestWorkers are actually busy doing work. Setting it to 1 effectively disables the async benefit. Setting it to 4-5 is reasonable on very keep-alive-heavy workloads.

What "tuned wrong" actually looks like

Three failure modes we see in audits, each one a different way of getting MPM event wrong:

Failure 1: MaxRequestWorkers is the distro default. Debian/Ubuntu ship with MaxRequestWorkers 150 for event. On a host with 8GB of RAM serving a static-heavy site, that's leaving 90% of available concurrency on the floor. Symptoms: high TCP SYN queues during traffic peaks, p95 latency that spikes randomly even though CPU and memory look fine. The host is artificially throttled.

Failure 2: MaxRequestWorkers is way too high. Someone read a guide that said "Apache should handle thousands of concurrent connections" and set MaxRequestWorkers 4000. The PHP-FPM pool downstream has 80 workers. The first time real traffic hits, Apache accepts 4000 connections, PHP-FPM is saturated within seconds, the TCP backlog to the FPM socket fills up, and Apache starts returning 503 because FastCGI timeouts fire. Latency goes to lunch. The fix isn't more Apache; it's matching Apache's capacity to the backend.

Failure 3: prefork with bumped numbers. Customer noticed performance problems, increased MaxRequestWorkers on a prefork server from 150 to 800. Each prefork child eats 50-80MB of RAM. 800 * 70MB = 56GB of resident memory just for Apache. The host has 16GB. OOM-killer eats the box during the next traffic spike. The right answer was always to move off prefork.

For a managed Apache install on the cloud, we provision with event from day one and the numbers are part of the base provisioning template, tied to the instance size.

When Apache still beats Nginx

We deploy a lot more Nginx than Apache in 2026. That's just the reality of modern stacks — Nginx is the default for terminating TLS in front of an application server, it's the default in Kubernetes ingress controllers, and the configuration model is simpler for that use case.

But Apache wins in a few specific places, and they're worth being honest about:

.htaccess and per-directory configuration. This is Apache's killer feature for shared hosting and CMS-heavy environments. WordPress, Drupal, Magento all expect to drop rewrite rules in .htaccess and have them work without a server reload. Nginx has no equivalent; every URL rewrite is a global config change requiring a reload. If you're hosting dozens of different sites with different admin teams editing rewrites independently, Apache's model is genuinely better.

Rich URL rewriting with mod_rewrite. mod_rewrite is more expressive than Nginx's rewrite directive. Conditional logic, lookup tables (RewriteMap), back-references that work the way you'd expect — it's a fully-featured rewriting language. We have customers with complex legacy URL schemes that would take weeks to port to Nginx and run them on Apache because there's no win in changing.

Module ecosystem. mod_security, mod_evasive, mod_authnz_ldap, mod_jk (for legacy Tomcat fronting), mod_perl (yes, still). The Apache module ecosystem is broader and more mature than Nginx's. If you need an authentication backend Nginx doesn't speak natively, Apache probably has a module for it.

Operator familiarity. This one is fuzzy but real — Apache has been around since 1995, every sysadmin over 30 has run it, and the config format is well-documented. For a small team without dedicated SRE, Apache is genuinely more legible.

We use Nginx where it's the better tool. We use Apache where it's the better tool. Customers who insist on one or the other regardless of fit usually pay for it in maintenance overhead. The pragmatic answer is mostly "use what the application expects" and only migrate when there's a clear win.

What we monitor on every Apache box

Sizing the MPM correctly is necessary but not sufficient. The numbers shift as the workload shifts; you have to watch the right metrics:

  • BusyWorkers and IdleWorkers from mod_status — the ratio tells you how close to saturation you are. Aim to spend most of your time with BusyWorkers < 60% of MaxRequestWorkers.
  • ConnsTotal and ConnsAsyncKeepAlive — confirms event is actually doing async keep-alive offload, not silently degrading to worker behaviour.
  • Per-child RSS over time — flags memory leaks that MaxConnectionsPerChild is recycling, but slowly enough to matter.
  • TCP SYN queue depth on port 443 — when this grows, Apache is at its MaxRequestWorkers ceiling.
  • PHP-FPM listen.queue depth — the downstream equivalent. If FPM's queue is filling but Apache's isn't, the bottleneck is FPM, not Apache.

Without these you're sizing the MPM blind. With them you've got the feedback loop to keep the numbers right as the workload evolves.

Defaults we ship

For a customer running Apache on AWS, GCP, Azure, or DigitalOcean under our management, here's what gets deployed on day one:

  • mpm_event enabled, prefork and worker modules disabled
  • mod_php removed; PHP-FPM with mod_proxy_fcgi over a Unix socket
  • MaxRequestWorkers sized to roughly 2x the PHP-FPM pool size
  • MaxConnectionsPerChild = 10000
  • HTTP/2 enabled with mod_http2
  • mod_status exposed on a local-only endpoint scraped by Prometheus
  • mod_security with OWASP CRS, tuned per-application (more on that in the next post)

The whole thing is deployable in roughly 20 minutes for a single host, or as part of a fleet provisioning run for larger estates.

If your Apache servers are still on prefork or have been running on distribution defaults since they were installed, we can usually demonstrate a 3-5x throughput improvement and a meaningful drop in p95 latency just from this MPM work. It's the kind of project that pays for itself in the first month.

Sudhanshu K. is a senior infrastructure engineer at EdgeServers (RemotIQ Pty Ltd, ABN 91 682 628 128). He has been running Apache in production since 1.3 and quietly thinks the config format is better than Nginx's, fight him about it.