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

AspectApache 2.4Nginx
Connection modelMulti-Processing Modules (MPM): prefork, worker or eventEvent-driven, a few worker processes handle thousands of connections each
Memory under many idle connectionsHigher, especially with preforkLow and predictable
PHPPHP-FPM via mod_proxy_fcgi, or embedded mod_phpPHP-FPM via FastCGI only
Per-directory config.htaccess files, no reload neededNot supported; all config is central
ModulesLoaded dynamically with a2enmodCompiled in, or dynamic modules packaged separately
Reverse proxy and load balancingmod_proxy, mod_proxy_balancerBuilt in (proxy_pass, upstream)
Cachingmod_cache, mod_cache_diskproxy_cache, fastcgi_cache
Config testsudo apache2ctl configtestsudo nginx -t
Package on Ubuntu 24.04apache2 (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 prefork and mod_php is 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 prefork can run out of processes (MaxRequestWorkers) and queue requests.
  • .htaccess: when AllowOverride is enabled, Apache checks for .htaccess files 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:

  1. Change Apache's Listen 80 in /etc/apache2/ports.conf to Listen 127.0.0.1:8080 and update <VirtualHost *:80> to <VirtualHost 127.0.0.1:8080>.
  2. Proxy from Nginx with proxy_pass http://127.0.0.1:8080; and the headers shown in the reverse proxy example.
  3. Enable sudo a2enmod remoteip and add RemoteIPHeader X-Forwarded-For and RemoteIPInternalProxy 127.0.0.1 to 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.