Nginx vs Apache: An Honest Comparison (With Benchmarks)

Most “nginx vs Apache” articles are the same article. Nginx is event-driven and fast, Apache is process-based and flexible, pick nginx for traffic and Apache for .htaccess. Some of that is true. Some of it was true in 2012 and hasn’t been updated since. And almost none of those articles show you a single number they measured themselves.

So I did the boring thing. Same machine, same files, same PHP-FPM pool, both servers on their default Ubuntu configs, and a load generator pointed at each in turn. Then I measured memory properly, because the usual way of measuring it gives you the wrong answer. Here’s what came out, including the parts where Apache did better than its reputation.

The short answer

For static files and lots of simultaneous connections, nginx is clearly faster and lighter under load. For PHP, the difference is small, because both hand the work to the same PHP-FPM and that’s where the time goes. Apache’s real advantage isn’t speed, it’s .htaccess and a huge module ecosystem, and you pay a measurable price for the first one. If you’re starting fresh on your own server, nginx is the sensible default. If you’re on shared hosting or running an app that leans on .htaccess, Apache is still a perfectly good choice, and “Apache is slow” is mostly a myth left over from mod_php and prefork days.

How they actually differ

Connection handling

This is the difference everyone quotes, and it’s usually quoted out of date.

Nginx runs a small number of worker processes, and each one handles thousands of connections in a single event loop. A slow client sitting on a connection costs almost nothing, it’s just an entry in a list the worker checks when data arrives.

Apache has pluggable Multi-Processing Modules (MPMs), and which one you’re on changes everything:

  • prefork: one process per connection. This is the “Apache eats memory” Apache. It was the default for years because mod_php required it.
  • worker: processes with multiple threads each.
  • event: threads, plus a dedicated listener that parks idle keep-alive connections so they don’t tie up a thread. This is nginx’s trick, borrowed, and it’s the default on modern distros.

So “Apache uses a process per request” is only true if you’re still on prefork. Ubuntu’s default is event:

$ sudo apachectl -V | grep -i mpm
Server MPM:     event

Event narrows the gap. It doesn’t close it, as the numbers below show.

Configuration

Apache reads its main config at startup, but it also checks for .htaccess files in every directory along the request path, on every request, if AllowOverride allows it. That’s what lets a WordPress plugin or a shared-hosting customer change rewrite rules without touching the server config. Nginx has nothing like it. All config lives in the server files, and changes need a reload. Less flexible, faster, and much easier to reason about, since there’s exactly one place to look.

Same job, both syntaxes. Sending every request that isn’t a real file to index.php, the way WordPress and Laravel expect:

# Apache (.htaccess or vhost)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]

# nginx (server block)
location / {
    try_files $uri $uri/ /index.php?$query_string;
}

PHP

Apache can run PHP inside itself with mod_php, and nginx can’t run PHP at all, it always hands it to PHP-FPM. That used to be a real architectural difference. Today it mostly isn’t, because Apache with PHP-FPM through mod_proxy_fcgi is the recommended setup, and it lets Apache stay on the event MPM. That’s exactly how I set up both servers here: identical PHP-FPM pool, identical socket.

The test setup

Both servers ran on the same Ubuntu 26.04 VM (22 CPU cores, 16 GB RAM), side by side on different ports, serving the same directory:

$ apache2 -v | head -1
Server version: Apache/2.4.66 (Ubuntu)

$ nginx -v
nginx version: nginx/1.28.3 (Ubuntu)

$ curl -s http://127.0.0.1:8080/hello.php; curl -s http://127.0.0.1:8081/hello.php
hello from PHP 8.5.4
hello from PHP 8.5.4

Apache on 8080, nginx on 8081, both passing .php to the same php8.5-fpm socket. Test files: a 1 KB text file, a 100 KB text file, and a one-line PHP script. Configs were left at Ubuntu’s defaults, because that’s what most people actually run. The defaults that matter:

$ grep -vE '^\s*#|^\s*$' /etc/apache2/mods-enabled/mpm_event.conf
StartServers            2
MinSpareThreads         25
MaxSpareThreads         75
ThreadLimit             64
ThreadsPerChild         25
MaxRequestWorkers       150
MaxConnectionsPerChild  0

$ grep -E '^\s*worker_(processes|connections)' /etc/nginx/nginx.conf
worker_processes auto;
	worker_connections 768;

The load generator was wrk (4 threads, 10 second runs, after a warm-up), running on the same machine. That last part matters. It means the client and servers competed for CPU, the absolute numbers are higher than you’d get over a real network, and they vary between runs. I ran the key tests twice and I’m showing both. Treat the numbers as a comparison between the two servers on identical hardware, not as a prediction for your VPS.

The results

Static files

Test Apache (event) nginx nginx ahead by
1 KB file, 50 connections 43,851 req/s 142,744 req/s 3.3×
1 KB file, 50 connections (second run) 30,501 req/s 110,474 req/s 3.6×
1 KB file, 500 connections 26,272 req/s 107,422 req/s 4.1×
100 KB file, 50 connections 20,701 req/s 77,886 req/s 3.8×

Not close. Across every static test, nginx served 3 to 4 times as many requests. Notice the run-to-run spread too: 142k and 110k for the same nginx test. That’s the shared-machine effect, and it’s why I’d never trust a single benchmark number, mine included.

Latency tells the same story. At 50 connections on the 1 KB file, half of nginx’s responses came back in 215 µs against Apache’s 1.03 ms. At 500 connections, Apache’s median went to 13.7 ms, nginx’s to 1.9 ms. Here’s one full nginx run at 500 connections, so you can see what wrk actually reports:

Running 10s test @ http://127.0.0.1:8081/1k.txt
  4 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    24.51ms   58.96ms 559.12ms   91.71%
    Req/Sec    26.99k    19.41k   80.47k    63.13%
  Latency Distribution
     50%    3.44ms
     75%   15.35ms
     90%   67.45ms
     99%  303.80ms
  1023666 requests in 10.08s, 1.31GB read
Requests/sec: 101567.49
Transfer/sec:    132.60MB

One honest caveat on the 500-connection test: Apache’s default MaxRequestWorkers 150 means it can only work on 150 connections at once, and the rest queue. You can raise it. That’s a real tuning step, though, and nginx needed no tuning at all to handle the same load.

wrk also reported “read” socket errors against Apache (450 in the first 50-connection run), and none against nginx. My first guess was Apache’s keep-alive limit, MaxKeepAliveRequests 100, closing connections that wrk then counts as errors. So I raised it to 1000 and re-ran. The errors dropped to 74, but throughput didn’t change (that’s the 30,501 row above). Keep-alive settings weren’t what was holding Apache back.

PHP

Test Apache (event) nginx nginx ahead by
hello.php, 50 connections 16,133 req/s 19,696 req/s 22%
hello.php, 50 connections (second run) 10,820 req/s 11,669 req/s 8%

This is the result that should shape your decision more than the static one. Once PHP is involved, both servers spend most of their time waiting on PHP-FPM, so the web server matters much less. Nginx still wins, by 8 to 22%, but on a real WordPress or Laravel page, where PHP and the database take tens of milliseconds, that gap shrinks further. If you’re choosing a web server to make a PHP app faster, you’re probably optimizing the wrong layer. OPcache, a page cache and a sensible PHP-FPM pool size will do far more.

Memory, and why the usual measurement lies

The standard way to “compare memory” is to add up the RSS of each server’s processes. Here’s what that gives you, idle:

nginx: 23 processes, 115.9 MB total
apache2: 3 processes, 18.0 MB total

Nginx using six times more memory than Apache? That’s not right, and the reason is how RSS works. Nginx started worker_processes auto, one worker per CPU core, so 22 workers plus a master on this 22-core box. They share most of their memory pages, and summing RSS counts every shared page 23 times. PSS (proportional set size) splits shared pages fairly between the processes using them, so its sum is the real footprint:

State Processes PSS (real) RSS sum (misleading)
nginx, idle, 22 workers 23 20.8 MB 136.0 MB
nginx, 500 connections 23 20.7 MB 136.0 MB
nginx, idle, 4 workers 5 4.7 MB 20.9 MB
Apache event, idle 3 5.2 MB 17.1 MB
Apache event, 500 connections 7 25.5 MB 62.7 MB
Apache prefork, 500 connections 151 47.0 MB 826.0 MB

A few honest takeaways from that table:

  • Idle, Apache is actually smaller than nginx on a many-core machine, because nginx pre-starts a worker per core. On a typical 2 or 4 core VPS, nginx is around 5 MB.
  • Under load, nginx doesn’t grow. 20.8 MB idle, 20.7 MB with 500 connections. Apache event grew almost fivefold as it spawned processes.
  • Prefork is where the “Apache eats RAM” reputation comes from: 151 processes. And look at the RSS column. 826 MB is the number people screenshot, while the real cost was 47 MB. Scary, but not the memory disaster it looks like.
  • Nginx with 4 workers still served 118,892 req/s on the 1 KB test. Running a worker per core on a 22-core machine is way more than you need.

If RSS, PSS and “used” memory are fuzzy, our guide to reading the free command explains how Linux counts memory in general.

Prefork versus event

I switched Apache to prefork for a clean run, since plenty of servers still use it (anything with mod_php does):

$ sudo apachectl -V | grep -i mpm
Server MPM:     prefork
Test Apache prefork Apache event
1 KB file, 50 connections 64,294 req/s 43,851 req/s
1 KB file, 500 connections 45,252 req/s (p99 1.11 s) 26,272 req/s (p99 343 ms)

I didn’t expect this. On raw throughput for tiny static files, prefork actually beat event, and it had no socket errors. The cost shows up elsewhere: 151 processes, nearly double event’s real memory, and a 99th percentile latency over a second at 500 connections. So the slowest 1 in 100 visitors waited more than a second. Throughput numbers alone would have told you prefork was the better choice. It isn’t, for anything facing real traffic. Neither Apache MPM got anywhere near nginx.

What .htaccess costs

I turned on AllowOverride All and dropped an empty .htaccess into the directory, so Apache had to look for and read it on every request:

Apache event, 1 KB file, 50 connections req/s
AllowOverride None 43,851
AllowOverride All with an empty .htaccess 30,839

About 30% slower, for an empty file. That’s one run each on a shared machine, so read it as “noticeable” rather than “exactly 30%”. The reason is that Apache checks every directory from the document root down to the file, for every request. On a deep path, that’s several filesystem lookups before it serves a byte. It’s the price of the flexibility, and it’s why the Apache docs themselves say to put rules in the main config when you have access to it.

Things that don’t show up in a benchmark

Modules and features

Apache loads and unloads modules at runtime (a2enmod, a2dismod) and has decades of them: authentication backends, WebDAV, per-directory everything. Nginx supports dynamic modules too, but the ecosystem is smaller, and some features are only in the paid NGINX Plus (active health checks, for example). For reverse proxying, load balancing, caching and TLS termination, nginx’s built-in features are excellent. Our nginx load balancer guide shows how much you get without any extra modules.

Security

Honestly, no clear winner. Both have long track records, fast security releases, and get patched through your distro’s normal updates. Nginx’s smaller codebase and lack of .htaccess mean fewer surprises, since nobody can change server behaviour by uploading a file to a directory. Apache’s module system means more code you can accidentally leave enabled. Either way, the things that actually get servers compromised are the same: unpatched packages, exposed admin panels, weak SSH. Our server hardening checklist matters more than which web server you picked.

Documentation and familiarity

Underrated. If your team knows Apache and has years of working vhost configs, the switching cost is real, and the performance gain for a typical PHP site is small. Plenty of teams run Apache happily for exactly this reason.

Using both together

You don’t have to choose. A common setup puts nginx in front as a reverse proxy, serving static files and handling TLS and slow clients, with Apache behind it on a local port running the PHP app and its .htaccess rules. You get nginx’s efficiency on the connection side and Apache’s compatibility on the app side. The cost is two servers to configure and patch. It’s a good middle ground when you’re migrating an Apache app gradually.

So which should you use?

Pick nginx when:

  • You’re setting up a new server and you control the config.
  • You serve a lot of static files, images, or downloads.
  • You expect many simultaneous connections, or slow clients on mobile networks.
  • You’re putting it in front of Node, Python, Go or container apps as a reverse proxy.
  • Memory is tight and traffic is unpredictable.

Pick Apache when:

  • You’re on shared hosting, or users need per-directory control through .htaccess.
  • Your app ships .htaccess rules you don’t want to translate by hand.
  • You need a specific Apache module with no good nginx equivalent.
  • Your team already knows Apache well and the site is mostly dynamic PHP.

If you do pick Apache, use the event MPM with PHP-FPM, set AllowOverride None wherever you don’t need .htaccess, and raise MaxRequestWorkers if you expect real concurrency. Those three changes remove most of the gap people blame on Apache itself.

Setup confirmation: the test rig

Before trusting any of the numbers above, I checked that both servers really were serving identical content through identical PHP, with the MPM and worker settings stated:

Check Result
Apache running, event MPM ✅ Apache/2.4.66, Server MPM: event
nginx running, config valid ✅ nginx/1.28.3, nginx -t successful
Same static file from both ✅ 1k.txt, Content-Length: 1024 on both
Same PHP-FPM behind both ✅ both return hello from PHP 8.5.4
Load generator available ✅ wrk 4.1.0 with epoll
Apache back on event after the prefork test ✅ Server MPM: event
$ curl -sI http://127.0.0.1:8080/1k.txt | grep -iE '^(server|content-length)'
Server: Apache/2.4.66 (Ubuntu)
Content-Length: 1024

$ curl -sI http://127.0.0.1:8081/1k.txt | grep -iE '^(server|content-length)'
Server: nginx
Content-Length: 1024

$ wrk --version | head -1
wrk debian/4.1.0-4build3 [epoll] Copyright (C) 2012 Will Glozer

$ systemctl is-active nginx apache2 php8.5-fpm | xargs
active active active

Want to repeat the test yourself? Install both (sudo apt install nginx apache2 wrk), move Apache to another port in /etc/apache2/ports.conf, point both at the same folder and PHP-FPM socket, then run wrk -t4 -c50 -d10s --latency against each. Run every test more than once. If you’re new to watching services while you test, the systemd guide and our journalctl commands will help when something doesn’t start.

Frequently asked questions

Is nginx always faster than Apache?

For static files and high concurrency, in my tests, yes, by 3 to 4 times. For PHP pages the gap was 8 to 22%, and on real applications it’s smaller still, because PHP and the database dominate. “Always” is the wrong word. “Faster where the web server is actually the bottleneck” is closer.

Why does nginx show more memory than Apache in top?

Because worker_processes auto starts one worker per CPU core, and tools that add up RSS count shared memory once per process. Measured with PSS, idle nginx on my 22-core machine was 20.8 MB, and with 4 workers it was 4.7 MB. Set worker_processes to your core count on small servers, or leave it on auto, which does exactly that.

Can nginx read .htaccess files?

No, and it never will. It’s a deliberate design choice, and the benchmark above shows why. Rules have to be translated into the server block. For most apps, a single try_files line replaces the whole .htaccess rewrite section.

Does WordPress run better on nginx or Apache?

Both run it well. WordPress assumes Apache out of the box, because plugins write to .htaccess, so on nginx some caching and security plugins need their rules added by hand. Performance-wise, a page cache matters far more than the web server underneath it.

Should I switch my existing Apache server to nginx?

Only if you have a reason: memory pressure, lots of static traffic, or high concurrency you’re struggling with. Check first that Apache is on event with PHP-FPM, not prefork with mod_php. That switch alone often fixes the problem that made people want to leave.

Which one has the bigger market share?

They swap places depending on who’s counting and how, and it doesn’t matter much for your choice. Both are mature, widely deployed, and not going anywhere.

Where this leaves you

The honest summary: nginx is genuinely faster and more predictable when connections and static files are the workload, and it stays lean under load. Apache is slower on static content and pays a real cost for .htaccess, but the old “Apache is bloated” story is mostly about prefork and mod_php, and the gap on PHP pages is small. Choose based on what your site actually does and what your team can maintain, not on a 3× number from a static-file test. Including mine.

Avatar photo

Asif Khan

I am a DevOps, Linux, and Cloud engineer with 10+ years building infrastructure that stays up, and automation so I do not have to fix it at 2am. My focus is automation, security, and systems built to survive failure, using Infrastructure as Code, CI/CD, containers, monitoring, and cloud platforms instead of manual fixes. This blog is where I write down what worked, and just as often what didn't the first time around. Real tutorials you can run yourself, troubleshooting notes pulled from actual outages, and the kind of detail you only get once you've hit the problem firsthand. If you're trying to get Linux, DevOps, and cloud infrastructure to behave, that's what you'll find here: reproducible solutions you can understand, test, and apply in your own environment.