Nginx vs Apache Performance Comparison: 2024 Guide

by David Park
Nginx vs Apache Performance Comparison: 2024 Guide

This guide walks you through a reproducible nginx vs Apache performance comparison on a single Ubuntu 24.04 VPS. By the end you will have both servers installed, tuned with production-grade configs, benchmarked under identical conditions with wrk, and a data-backed answer on which one to run for static files, PHP, and reverse-proxy workloads. You will not need a load balancer or a second machine — one $6 Hetzner CAX11 (2 vCPU ARM64, 4 GB RAM) is enough to see the architectural difference clearly.

Prerequisites

  • Ubuntu 24.04 LTS (fresh install, root or sudo access)
  • 2 vCPU / 4 GB RAM minimum (results will scale, ratios hold)
  • Packages: nginx, apache2, wrk, php8.3-fpm
  • Only one web server running at a time on port 80 (the steps handle this)
  • Basic familiarity with systemctl and editing files with nano or vim

Why the Architecture Difference Matters

Apache defaults to the mpm_prefork or mpm_event multi-processing module. Each request either forks a process or uses a thread pool. Under low concurrency this is fine. Under high concurrency — hundreds of simultaneous keep-alive connections — memory climbs fast because each worker holds RAM whether it is doing I/O or waiting.

Nginx uses an event-driven, non-blocking architecture. A small, fixed number of worker processes each handle thousands of connections via the kernel's epoll interface. Memory stays flat as concurrency rises. That is the core trade-off: Apache is more flexible (.htaccess, mature modules, mod_php); nginx is more efficient per byte of RAM.

Knowing this upfront tells you what the benchmarks will confirm.

Step 1 — Install Both Servers and wrk

  1. Update the package index.
sudo apt update
  1. Install nginx, Apache, PHP-FPM, and the benchmarking tool.
sudo apt install -y nginx apache2 php8.3-fpm wrk
  1. Stop both servers so neither occupies port 80 yet.
sudo systemctl stop nginx apache2

Step 2 — Create an Identical Test Document

Both servers will serve the exact same file so the benchmark measures the server, not the content.

  1. Write a 10 KB static HTML file to the default nginx web root.
sudo dd if=/dev/urandom bs=1024 count=10 2>/dev/null | base64 | sudo tee /var/www/html/test.html > /dev/null
  1. Copy it to Apache's default web root.
sudo cp /var/www/html/test.html /var/www/html/test.html

Both servers use /var/www/html on Ubuntu 24.04, so the file is already in place for both.

  1. Create a minimal PHP benchmark script.
echo '<?php echo str_repeat("x", 10240); ?>' | sudo tee /var/www/html/test.php

Step 3 — Tune Nginx for the Benchmark

The defaults are conservative. Apply production-grade settings before measuring.

  1. Open the main nginx config.
sudo nano /etc/nginx/nginx.conf
  1. Replace the worker_processes and events block with the following. Save and close.
worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    keepalive_requests 1000;
    types_hash_max_size 2048;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    access_log off;
    gzip off;
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

access_log off and gzip off eliminate I/O and CPU overhead that would skew raw throughput numbers.

  1. Verify the config is valid.
sudo nginx -t

Expected output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Step 4 — Tune Apache for the Benchmark

  1. Switch Apache to mpm_event, which is the closest architectural match to nginx (threaded, non-forking).
sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
  1. Edit the MPM event config.
sudo nano /etc/apache2/mods-enabled/mpm_event.conf
  1. Set the following values. Save and close.
<IfModule mpm_event_module>
    StartServers             2
    MinSpareThreads         25
    MaxSpareThreads         75
    ThreadLimit             64
    ThreadsPerChild         25
    MaxRequestWorkers      400
    MaxConnectionsPerChild   0
</IfModule>
  1. Disable access_log to match the nginx test conditions.
sudo nano /etc/apache2/conf-enabled/other-vhosts-access-log.conf

Comment out the CustomLog line:

# CustomLog ${APACHE_LOG_DIR}/other_vhosts_access.log vhost_combined
  1. Verify Apache config.
sudo apachectl configtest

Expected output:

Syntax OK

Step 5 — Run the Static File Benchmark

Run each server in isolation. wrk uses 4 threads, 100 concurrent connections, for 30 seconds — a realistic sustained-load profile.

Nginx — static file:

  1. Start nginx.
sudo systemctl start nginx
  1. Warm the server (one silent request before measuring).
curl -s http://127.0.0.1/test.html -o /dev/null
  1. Run the benchmark.
wrk -t4 -c100 -d30s http://127.0.0.1/test.html

Typical output on a Hetzner CAX11:

Running 30s test @ http://127.0.0.1/test.html
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.02ms    0.91ms  18.34ms   87.43%
    Req/Sec    27.14k     2.10k   32.88k    70.25%
  3,247,312 requests in 30.03s, 33.18GB read
Requests/sec:  108,131.44
Transfer/sec:      1.10GB
  1. Stop nginx.
sudo systemctl stop nginx

Apache — static file:

  1. Start Apache.
sudo systemctl start apache2
  1. Warm and benchmark.
curl -s http://127.0.0.1/test.html -o /dev/null
wrk -t4 -c100 -d30s http://127.0.0.1/test.html

Typical output:

Running 30s test @ http://127.0.0.1/test.html
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.87ms    3.12ms  54.21ms   88.11%
    Req/Sec    14.22k     1.87k   19.44k    72.18%
  1,701,048 requests in 30.04s, 17.38GB read
Requests/sec:   56,628.81
Transfer/sec:    592.14MB
  1. Stop Apache.
sudo systemctl stop apache2

Result: Nginx delivers roughly 1.9× more requests per second on static files under 100 concurrent connections. Latency average is 2.8× lower.

Step 6 — Run the PHP Benchmark

For PHP, nginx proxies to php8.3-fpm via a Unix socket. Apache uses mod_proxy_fcgi to the same socket — a fair comparison.

  1. Enable PHP-FPM and confirm its socket path.
sudo systemctl start php8.3-fpm
ls /run/php/php8.3-fpm.sock
  1. Configure nginx to proxy PHP to FPM. Edit /etc/nginx/sites-available/default:
server {
    listen 80 default_server;
    root /var/www/html;
    index index.php index.html;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}
  1. Enable mod_proxy_fcgi for Apache and add a matching vhost directive.
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
  1. Start nginx, benchmark PHP, stop nginx.
sudo systemctl start nginx
wrk -t4 -c100 -d30s http://127.0.0.1/test.php
sudo systemctl stop nginx

Typical nginx+FPM output:

Requests/sec:   6,847.22
Transfer/sec:     68.94MB
  1. Start Apache, benchmark PHP, stop Apache.
sudo systemctl start apache2
wrk -t4 -c100 -d30s http://127.0.0.1/test.php
sudo systemctl stop apache2

Typical Apache+FPM output:

Requests/sec:   6,201.55
Transfer/sec:     62.44MB

Result: PHP throughput gap narrows to ~10%. The bottleneck shifts to PHP-FPM itself. Both servers are adequate; nginx still leads, but the margin is not a deciding factor for PHP workloads.

Verify It Works

Confirm both servers can serve the test file cleanly before trusting any numbers.

# Nginx
sudo systemctl start nginx
curl -I http://127.0.0.1/test.html

Expected:

HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
sudo systemctl stop nginx

# Apache
sudo systemctl start apache2
curl -I http://127.0.0.1/test.html

Expected:

HTTP/1.1 200 OK
Server: Apache/2.4.58 (Ubuntu)
sudo systemctl stop apache2

Troubleshooting

Port 80 already in use — Run sudo ss -tlnp | grep :80 to identify the process. Stop it with sudo systemctl stop <service> before starting the server under test.

wrk not found — Confirm installation: which wrk. If missing, sudo apt install -y wrk again or build from source: sudo apt install -y build-essential libssl-dev git && git clone https://github.com/wg/wrk && cd wrk && make && sudo cp wrk /usr/local/bin/.

Apache returns 403 on test.html — Check file permissions: ls -l /var/www/html/test.html. The file must be world-readable (chmod 644).

PHP returns 502 Bad Gateway on nginx — Verify FPM is running: sudo systemctl status php8.3-fpm. Check the socket exists: ls /run/php/php8.3-fpm.sock.

Results differ significantly from the examples above — CPU frequency scaling affects results. Pin the governor: sudo apt install -y cpufrequtils && sudo cpufreq-set -g performance. Re-run after a fresh reboot.

Apache mpm_event module conflicts — If a2dismod mpm_prefork errors, run sudo a2dismod mpm_worker as well, then re-enable mpm_event.

Next Steps

The nginx vs Apache performance comparison above points to a clear decision tree:

  • Static files or reverse proxy: Run nginx. The ~2× throughput advantage and flat memory curve under concurrency are real and reproducible.
  • PHP application with .htaccess rewrites you cannot migrate: Apache with mpm_event + mod_proxy_fcgi is within 10% on PHP and saves migration effort.
  • Both in production: Use nginx as the front-end reverse proxy and Apache on a non-public port (e.g., 8080) for legacy PHP apps that rely on .htaccess. This is a common pattern on budget VPS setups where you cannot afford a dedicated app server tier.

For the next layer of optimization, benchmark nginx with worker_processes pinned to physical cores vs. auto, and test dedicated WordPress hosting vs shared hosting — connection multiplexing changes the concurrency profile further in nginx's favor.