Apache HTTP Server and Nginx are the two most widely deployed open source web servers. Both serve static files, run PHP applications through PHP-FPM, terminate TLS and work as reverse proxies, so for most sites either one will do the job. The real differences are in how they handle connections, how they are configured and whether you need per-directory .htaccess files. This guide compares both with side-by-side configuration examples for Ubuntu 24.04 and explains which one to choose for common workloads.
Quick comparison
| Aspect | Apache 2.4 | Nginx |
|---|---|---|
| Connection model | Multi-Processing Modules (MPM): prefork, worker or event | Event-driven, a few worker processes handle thousands of connections each |
| Memory under many idle connections | Higher, especially with prefork | Low and predictable |
| PHP | PHP-FPM via mod_proxy_fcgi, or embedded mod_php | PHP-FPM via FastCGI only |
| Per-directory config | .htaccess files, no reload needed | Not supported; all config is central |
| Modules | Loaded dynamically with a2enmod | Compiled in, or dynamic modules packaged separately |
| Reverse proxy and load balancing | mod_proxy, mod_proxy_balancer | Built in (proxy_pass, upstream) |
| Caching | mod_cache, mod_cache_disk | proxy_cache, fastcgi_cache |
| Config test | sudo apache2ctl configtest | sudo nginx -t |
| Package on Ubuntu 24.04 | apache2 (2.4.58) | nginx (1.24) |
Architecture
Apache
Apache delegates connection handling to a Multi-Processing Module:
- prefork: one process per connection, no threads. Needed only for non-thread-safe modules such as the embedded
mod_php. Memory use grows quickly with concurrent connections. - worker: several processes, each with many threads. One thread per connection.
- event: like
worker, but idle keep-alive connections are handed to a listener thread, so they do not occupy a worker thread. This is the default on Ubuntu and the right choice with PHP-FPM.
Check which MPM a server is using:
sudo apache2ctl -M | grep mpm
mpm_event_module (shared)
If you see mpm_prefork_module, the server is probably running mod_php. Switching to PHP-FPM and the event MPM is the single biggest performance improvement for most Apache servers.
Nginx
Nginx runs one master process and a small number of worker processes (by default one per CPU core with worker_processes auto;). Each worker handles many connections in a non-blocking event loop, so thousands of slow or idle clients cost very little memory. Nginx never runs application code itself; PHP, Python or Node.js applications run in separate processes that Nginx talks to over FastCGI or HTTP.
Performance in practice
Performance differences depend far more on the configuration than on the server itself:
- Static files: Nginx is very efficient at serving static files and handling many concurrent connections. Apache with the event MPM is close for moderate traffic, but uses more memory as concurrency grows.
- PHP applications: the bottleneck is PHP and the database, not the web server. With both servers passing requests to the same PHP-FPM pool, response times are nearly identical. Apache with
preforkandmod_phpis the slow outlier because every connection, including ones for images and CSS, occupies a full PHP-enabled process. - Many slow clients or long keep-alives: Nginx keeps memory flat; Apache
preforkcan run out of processes (MaxRequestWorkers) and queue requests. .htaccess: whenAllowOverrideis enabled, Apache checks for.htaccessfiles in every directory of the path on every request. Disabling it where you do not need it saves filesystem lookups.
Do not rely on generic benchmark numbers. Measure your own site with a load testing tool such as wrk, from a different machine than the server:
sudo apt install wrk
wrk -t4 -c200 -d30s https://your_domain/
Running 30s test @ https://your_domain/
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 24.10ms 10.52ms 210.33ms 78.10%
Req/Sec 2.08k 310.12 2.90k 70.25%
248913 requests in 30.03s, 1.21GB read
Requests/sec: 8288.71
Transfer/sec: 41.26MB
Run the same test against both servers with the same content and PHP-FPM pool, and watch memory with free -h while it runs.
Configuration side by side
All examples assume Ubuntu 24.04, a site in /var/www/your_domain and the domain your_domain.
Virtual host
Apache site file, created with sudo nano /etc/apache2/sites-available/your_domain.conf:
<VirtualHost *:80>
ServerName your_domain
ServerAlias www.your_domain
DocumentRoot /var/www/your_domain
<Directory /var/www/your_domain>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/your_domain_error.log
CustomLog ${APACHE_LOG_DIR}/your_domain_access.log combined
</VirtualHost>
Enable it and reload:
sudo a2ensite your_domain.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
Nginx server block, created with sudo nano /etc/nginx/sites-available/your_domain:
server {
listen 80;
listen [::]:80;
server_name your_domain www.your_domain;
root /var/www/your_domain;
index index.html index.php;
access_log /var/log/nginx/your_domain_access.log;
error_log /var/log/nginx/your_domain_error.log;
location / {
try_files $uri $uri/ =404;
}
}
Enable it and reload:
sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
PHP with PHP-FPM
Install PHP-FPM (PHP 8.3 on Ubuntu 24.04):
sudo apt install php8.3-fpm
On Apache, enable the FastCGI proxy modules and the PHP-FPM configuration shipped by the package:
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
sudo systemctl reload apache2
On Nginx, add a PHP location inside the server block:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ /\.ht {
deny all;
}
Test and reload Nginx with sudo nginx -t && sudo systemctl reload nginx. In both cases, a file containing <?php phpinfo(); should show Server API: FPM/FastCGI. Delete it after checking.
Front controller rewrites
Frameworks such as Laravel, Symfony and WordPress send every request that does not match a file to index.php. On Apache this usually lives in .htaccess (requires sudo a2enmod rewrite):
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
Nginx has no .htaccess; the equivalent goes in the server block and replaces the default location /:
location / {
try_files $uri $uri/ /index.php?$query_string;
}
Reverse proxy to an application
To put a Node.js, Python or other application listening on port 3000 behind the web server, Apache needs sudo a2enmod proxy proxy_http headers and this inside the virtual host:
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
RequestHeader set X-Forwarded-Proto "http"
Nginx:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
TLS works the same way for both: sudo apt install certbot python3-certbot-apache or python3-certbot-nginx, then sudo certbot --apache or sudo certbot --nginx edits the configuration and sets up automatic renewal.
Basic hardening
Hide the exact version in headers and error pages. On Apache, edit /etc/apache2/conf-available/security.conf and set:
ServerTokens Prod
ServerSignature Off
On Nginx, add this inside the http block of /etc/nginx/nginx.conf:
server_tokens off;
Nginx also has built-in request rate limiting, useful for login pages. Define a zone in the http block:
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;
Then apply it in a location inside the server block:
location = /wp-login.php {
limit_req zone=login burst=10 nodelay;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Apache has no equivalent in the core packages; the usual options are mod_evasive or fail2ban reading the access log. After any change, test the configuration before reloading.
Using both: Nginx in front of Apache
A common hybrid setup puts Nginx on ports 80 and 443 to terminate TLS, serve static files and absorb slow clients, and proxies dynamic requests to Apache on a local port. This keeps .htaccess support for applications that depend on it.
In that setup:
- Change Apache's
Listen 80in/etc/apache2/ports.conftoListen 127.0.0.1:8080and update<VirtualHost *:80>to<VirtualHost 127.0.0.1:8080>. - Proxy from Nginx with
proxy_pass http://127.0.0.1:8080;and the headers shown in the reverse proxy example. - Enable
sudo a2enmod remoteipand addRemoteIPHeader X-Forwarded-ForandRemoteIPInternalProxy 127.0.0.1to Apache so logs and applications see the real client IP.
It adds a moving part, so use it only if you need .htaccess; otherwise a single Nginx or a single Apache with the event MPM is simpler.
Which one should you choose?
Choose Apache when:
- Your application or users rely on
.htaccess(shared hosting, many WordPress plugins, legacy PHP apps). - You need modules that exist only for Apache, such as some authentication modules.
- Your team already knows Apache configuration and your traffic is moderate.
Choose Nginx when:
- You serve many concurrent connections, large static files or media.
- The server is mainly a reverse proxy or load balancer in front of Node.js, Python, Go or container workloads.
- You run on a small VPS where memory is limited.
- You want built-in rate limiting and simple caching of upstream responses.
For a new PHP site you control, either works; Nginx with PHP-FPM is the more common choice today, and Apache with the event MPM and PHP-FPM performs very similarly.
Conclusion
Apache and Nginx are both mature and fast enough for almost any site once they are configured correctly: Apache with the event MPM and PHP-FPM, Nginx with PHP-FPM or as a reverse proxy. Pick Apache when you need .htaccess and its module ecosystem, and Nginx when you need high concurrency, low memory use or a reverse proxy. As a next step, secure your chosen server with Let's Encrypt using Certbot, and load test your real application with wrk before and after tuning.
