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
systemctland editing files withnanoorvim
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
- Update the package index.
sudo apt update
- Install nginx, Apache, PHP-FPM, and the benchmarking tool.
sudo apt install -y nginx apache2 php8.3-fpm wrk
- 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.
- 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
- 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.
- 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.
- Open the main nginx config.
sudo nano /etc/nginx/nginx.conf
- Replace the
worker_processesandeventsblock 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.
- 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
- Switch Apache to
mpm_event, which is the closest architectural match to nginx (threaded, non-forking).
sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
- Edit the MPM event config.
sudo nano /etc/apache2/mods-enabled/mpm_event.conf
- 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>
- Disable
access_logto 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
- 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:
- Start nginx.
sudo systemctl start nginx
- Warm the server (one silent request before measuring).
curl -s http://127.0.0.1/test.html -o /dev/null
- 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
- Stop nginx.
sudo systemctl stop nginx
Apache — static file:
- Start Apache.
sudo systemctl start apache2
- 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
- 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.
- Enable PHP-FPM and confirm its socket path.
sudo systemctl start php8.3-fpm
ls /run/php/php8.3-fpm.sock
- 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;
}
}
- Enable
mod_proxy_fcgifor Apache and add a matching vhost directive.
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
- 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
- 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
.htaccessrewrites you cannot migrate: Apache withmpm_event+mod_proxy_fcgiis 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.