Nginx vs Apache: which web server should you choose?
The choice between Nginx and Apache depends on your use case. Here is an objective comparison. Both are mature, actively maintained open-source projects, and together they serve a very large share of websites. Neither is "better" in absolute terms: they were designed at different times, with different priorities, and those original choices still explain their respective strengths today.
Architecture
Apache uses a process/thread-based model (prefork or worker). Each connection is handled by a dedicated process or thread.
Nginx uses an asynchronous, event-driven architecture. A single worker process can handle thousands of simultaneous connections.
In practice, Apache delegates this behaviour to a module called an MPM (Multi-Processing Module):
prefork: one process per connection, no threads. It uses the most memory, but it is the only one compatible withmod_php, because PHP was historically not guaranteed to be thread-safe.worker: several processes, each with several threads. More memory-efficient.event: an evolution ofworkerthat hands idle keep-alive connections to a dedicated thread, instead of blocking one thread per client. It is the default MPM on recent distributions whenmod_phpis not installed.
Nginx, for its part, starts a small number of worker processes (often one per core, with worker_processes auto;). Each one handles thousands of connections in an event loop, using epoll on Linux. An idle connection costs only a few kilobytes of memory, which explains its stability when facing a large number of slow clients or keep-alive connections.
Performance
# Benchmark with ab (Apache Bench)
ab -n 10000 -c 100 http://localhost/
# Nginx: ~15000 req/s for static content
# Apache: ~5000 req/s for static content
These figures are orders of magnitude and should be taken with caution: they vary enormously depending on hardware, file size, MPM configuration and the number of concurrent connections. Apache configured with the event MPM significantly narrows the gap. Always measure on your own infrastructure, ideally from a machine other than the server under test, and with a tool like wrk that generates a more realistic load than ab.
Above all, for a PHP application the web server is rarely the bottleneck. Response time is dominated by PHP execution, SQL queries and network calls. The difference between Nginx and Apache shows mostly on static files and under high concurrency.
PHP configuration
Apache with mod_php:
<VirtualHost *:80>
DocumentRoot /var/www/html/public
<Directory /var/www/html/public>
AllowOverride All
</Directory>
</VirtualHost>
With mod_php, the PHP interpreter is loaded into every Apache process, including those that only serve an image or a CSS file. AllowOverride All enables .htaccess files: convenient, but Apache must then look for those files in every directory along the path, on every request.
Nginx with PHP-FPM:
server {
root /var/www/html/public;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
Nginx does not execute PHP: it forwards .php requests to PHP-FPM over FastCGI, here through a Unix socket. The two services have independent process pools, sized separately. Using $realpath_root rather than $document_root resolves symbolic links, which matters with atomic deployments where current points to a new release.
For a Symfony application, the recommended configuration goes further: every request that does not match an existing file goes through index.php, and no other PHP file can be executed:
server {
listen 80;
server_name example.com;
root /var/www/html/public;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
location ~ \.php$ {
return 404;
}
}
The internal directive prevents calling /index.php directly in the URL, and the last block returns a 404 for any other PHP file: a script left behind in public/ cannot be executed.
Modern Apache: event and PHP-FPM
Apache is not stuck with the prefork + mod_php pair. It can also hand PHP over to PHP-FPM through mod_proxy_fcgi and use the event MPM. On Debian or Ubuntu:
sudo apt install php8.3-fpm
sudo a2dismod php8.3 mpm_prefork
sudo a2enmod mpm_event proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
sudo apachectl configtest && sudo systemctl restart apache2
The virtual host then forwards PHP files to the PHP-FPM socket. If you do not need .htaccess, disable it and put the rules directly in the configuration:
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html/public
<Directory /var/www/html/public>
AllowOverride None
Require all granted
FallbackResource /index.php
</Directory>
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>
</VirtualHost>
FallbackResource plays the same role as Nginx's try_files: any URL that does not match a file is served by index.php. With this configuration, Apache uses far less memory and gets close to Nginx in connection handling.
Quick comparison
| Criterion | Nginx | Apache |
|---|---|---|
| Model | Event-driven, asynchronous | Processes/threads via MPM (prefork, worker, event) |
| PHP | Through PHP-FPM only | mod_php or PHP-FPM |
| Per-directory configuration | No (central configuration) | Yes, with .htaccess |
| Static content | Very efficient | Decent, especially with event |
| Reverse proxy, load balancing | Native and very widespread | Possible via mod_proxy |
| Configuration test | nginx -t | apachectl configtest |
When to choose which?
- Nginx: static content, reverse proxy, high concurrency, microservices
- Apache: .htaccess required, specific modules, legacy compatibility
- Both: Nginx as a reverse proxy in front of Apache for the best of both worlds
The "both" setup is common on shared hosting or during a gradual migration: Nginx listens on the public ports, serves static files and handles TLS, then forwards the rest to Apache on a local port:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}
In this setup, enable mod_remoteip on the Apache side so that logs and the application see the client's real IP address rather than Nginx's. On the Symfony side, declare 127.0.0.1 in framework.trusted_proxies so that the X-Forwarded-* headers are taken into account.
Common pitfalls
- Mixing
mod_phpwith theeventMPM: not possible, Apache refuses to start. Switch to PHP-FPM to useevent. - Translating an
.htaccessblindly: when migrating to Nginx, rewrite rules must be ported by hand into the configuration, otherwise redirects or protections silently disappear. - Forgetting to test the configuration: always run
nginx -torapachectl configtestbefore reloading the service. - Sizing only one side: with PHP-FPM, it is often
pm.max_childrenthat limits throughput, not the web server.
In high-traffic contexts such as at CCM Benchmark, Nginx is preferred for its efficient handling of concurrent connections.
In summary: for a new PHP or Symfony project, Nginx with PHP-FPM is a solid and simple default choice. Apache remains relevant when .htaccess files are essential, when an application depends on specific modules, or when the team already masters its ecosystem. Either way, it is the configuration, and PHP-FPM's in particular, that will make the difference in production.