Nginx and Apache are the two most widely deployed web servers in the world, together powering the majority of sites on the internet. They solve the same problem — serving HTTP traffic — but they do it with completely different architectures. Choosing between them (or combining them) can meaningfully affect your performance, scalability, and ops overhead.
At a glance
| Nginx | Apache | |
|---|---|---|
| First release | 2004 | 1995 |
| Architecture | Event-driven, asynchronous, non-blocking | Process/thread-per-connection (default) |
| Concurrency model | Single-threaded event loop | Multi-Processing Modules (MPM) |
| Static file speed | Excellent | Good |
| Dynamic content | Via reverse proxy (PHP-FPM, uWSGI) | Via mod_php (in-process) or proxy |
| Config style | Block directives, no per-directory overrides | Directives + .htaccess per-directory |
| Memory under load | Very low, predictable | Higher, depends on MPM |
| Reverse proxy / LB | Built-in, high performance | mod_proxy (works, more overhead) |
| Market share (2025) | ~34% (Netcraft) | ~25% |
| Learning curve | Moderate | Moderate (.htaccess eases deployment) |
Architecture: the core difference
This is the most important thing to understand. Everything else flows from it.
Apache: process / thread per connection
By default (prefork MPM), Apache spawns a new process for every connection. Each process handles one request at a time:
Request 1 → Worker Process 1 (1 req × 1 proc)
Request 2 → Worker Process 2
Request 3 → Worker Process 3
...
1000 reqs → ~1000 processes (high RAM)
With event MPM (modern default), Apache becomes more like Nginx — threads handle keep-alive connections without blocking. But the model is still heavier than Nginx's.
Nginx: event-driven, non-blocking
Nginx uses a fixed pool of worker processes (typically one per CPU core). Each worker handles thousands of connections via an event loop — no blocking I/O:
1 Worker Process
├── Connection 1 (waiting for DB)
├── Connection 2 (sending file)
├── Connection 3 (reading request)
└── Connection 4 (writing response)
... (thousands more)
Result: Nginx uses far less RAM under high concurrency and handles the C10k problem natively.
Performance
Static files
Nginx is the clear winner for serving static assets. It was specifically designed to serve files efficiently from disk using sendfile() and kernel-level optimisations.
| Scenario | Nginx | Apache |
|---|---|---|
| Static HTML/CSS/JS | Very fast | Fast |
| Large file streaming | Excellent (sendfile) |
Good |
| 10k concurrent connections | ~2–4 MB RAM/worker | 20–50 MB RAM/connection (prefork) |
| Latency at p99 under load | Low | Higher (process spawn overhead) |
Dynamic content (PHP, Python, Node.js)
Neither server runs application code directly anymore. Both proxy to external processes:
| Nginx | Apache | |
|---|---|---|
| PHP | Proxy to PHP-FPM | mod_php (in-process) or PHP-FPM proxy |
| Python | Proxy to Gunicorn/uWSGI | mod_wsgi or proxy |
| Node.js | Proxy to Node process | mod_proxy to Node process |
mod_php (Apache only): Embeds PHP into every Apache worker process — simpler setup, but means every worker carries a full PHP interpreter, even when serving static files. Fine for small sites, wasteful at scale.
PHP-FPM (both): PHP runs in a separate pool of processes. Nginx + PHP-FPM is the most common high-performance PHP stack today (used by most managed WordPress hosts).
Configuration style
This is where the two servers feel most different day-to-day.
Nginx: block directives, centralised config
# /etc/nginx/sites-available/mysite.conf
server {
listen 80;
server_name example.com www.example.com;
root /var/www/mysite/public;
index index.html index.php;
# Serve static files directly; proxy PHP to FPM
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Cache static assets
location ~* \.(css|js|png|jpg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Key points:
- All config lives in
/etc/nginx/— no per-directory overrides - Changes require
nginx -s reload(graceful, no downtime) try_filesreplaces most rewrite rules- No
.htaccess— intentional (disk I/O on every request)
Apache: directives + .htaccess
# /etc/apache2/sites-available/mysite.conf
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/mysite/public
<Directory /var/www/mysite/public>
Options -Indexes +FollowSymLinks
AllowOverride All # enables .htaccess
Require all granted
</Directory>
</VirtualHost>
# /var/www/mysite/public/.htaccess (per-directory overrides)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
Header set Cache-Control "max-age=31536000, public"
Key points:
.htaccesslets each directory (even untrusted users) configure the server- Great for shared hosting — users can deploy PHP apps without root
- Apache re-reads
.htaccesson every request (small performance cost) mod_rewriteis powerful but has a steeper learning curve than Nginx rewrite
Reverse proxy and load balancing
Nginx was built as a reverse proxy from day one. Its upstream module is lean and handles thousands of backend connections without spawning extra processes:
upstream backend {
least_conn; # load balancing algorithm
server 10.0.0.1:3000;
server 10.0.0.2:3000;
server 10.0.0.3:3000 backup;
keepalive 64; # reuse connections
}
server {
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}
Apache's mod_proxy works, but adds overhead:
<Proxy balancer://backend>
BalancerMember http://10.0.0.1:3000
BalancerMember http://10.0.0.2:3000
ProxySet lbmethod=byrequests
</Proxy>
ProxyPass /api/ balancer://backend/
ProxyPassReverse /api/ balancer://backend/
For pure reverse proxy / API gateway workloads, Nginx is the default choice. For this reason Nginx is often used as the front-end even when Apache handles the PHP backend.
SSL/TLS
Both support TLS well. Nginx config is somewhat simpler:
# Nginx TLS (with Let's Encrypt / Certbot)
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_session_cache shared:SSL:10m;
}
# Apache TLS
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite HIGH:!aNULL:!MD5
</VirtualHost>
Certbot has first-class plugins for both (certbot --nginx and certbot --apache).
Modules and extensibility
| Nginx | Apache | |
|---|---|---|
| Module loading | Compiled in (static) or dynamic (.so) |
Dynamic at runtime (a2enmod) |
| Key modules | ngx_http_proxy, ngx_http_gzip, ngx_http_cache, lua-nginx-module | mod_rewrite, mod_security, mod_php, mod_wsgi |
| Hot-reload modules | Yes (nginx -s reload) |
Yes (graceful restart) |
| Custom scripting | Lua (OpenResty), NJS | mod_perl, mod_lua |
| WAF | ModSecurity (Nginx connector) | ModSecurity (native) |
Apache's runtime module loading (a2enmod ssl, a2enmod rewrite) makes it easy to enable/disable features without recompiling. Nginx's compiled modules require a rebuild, though most distros ship with common modules pre-built.
WordPress: which is better?
Both work well. Most managed WordPress hosts (Kinsta, WP Engine, Cloudways) use Nginx + PHP-FPM for performance. Shared hosts (cPanel/Plesk) often use Apache because .htaccess makes WordPress's permalink rewriting work out of the box with no server config.
| Nginx | Apache | |
|---|---|---|
| Permalink rewriting | Requires try_files in server config |
Automatic via .htaccess + mod_rewrite |
| Performance | Better under load | Fine for low-traffic sites |
| Plugin compatibility | Excellent | Excellent |
| Managed host default | Yes (most modern hosts) | Yes (shared/cPanel hosts) |
| W3 Total Cache / WP Rocket | Full support | Full support |
For Nginx, WordPress's permalink rewriting needs this in your server block:
location / {
try_files $uri $uri/ /index.php?$args;
}
When to use Nginx
- High-concurrency applications (APIs, streaming, real-time)
- Reverse proxy / load balancer in front of app servers
- Serving static assets (CDN origin, SPA builds)
- Microservices architecture (API gateway)
- Docker / Kubernetes ingress (Nginx Ingress Controller is the most popular K8s ingress)
- High-traffic WordPress or Laravel with PHP-FPM
When to use Apache
- Shared hosting environments (
.htaccessis required) - Deploying apps that rely on
.htaccessfor rewriting (legacy PHP apps) - Teams that are already familiar with Apache config
- Environments that need easy per-directory config by non-root users
- When mod_security or mod_wsgi integration is required without extra plumbing
Nginx + Apache together
A common pattern is to put Nginx in front of Apache as a reverse proxy. Nginx handles:
- Static files (serves them directly, never touches Apache)
- SSL termination
- Gzip compression
- Rate limiting
- Caching
Apache handles:
- Dynamic PHP via mod_php (no PHP-FPM needed)
.htaccessrules (existing app compatibility)
Internet → Nginx (port 80/443) → Apache (port 8080, localhost only)
This was popular in the 2010s for legacy apps that couldn't easily migrate away from mod_php. Today, most new deployments go Nginx-only with PHP-FPM.
Full comparison
| Feature | Nginx | Apache |
|---|---|---|
| Architecture | Event-driven, async | Process/thread-based (event MPM available) |
| Concurrency | Excellent (C10k native) | Good (event MPM), limited (prefork) |
| Static file performance | Excellent | Good |
| Memory efficiency | Very high | Moderate |
| Config syntax | Block directives | Directives + .htaccess |
| Per-directory overrides | No | Yes (.htaccess) |
| Module loading | Mostly compiled | Dynamic at runtime |
| Reverse proxy | First-class | mod_proxy (works, more overhead) |
| Load balancing | Built-in, fast | mod_proxy_balancer |
| PHP integration | PHP-FPM proxy | mod_php or PHP-FPM |
| SSL/TLS | Excellent | Excellent |
| HTTP/2 | Yes | Yes (mod_http2) |
| HTTP/3 / QUIC | Experimental (1.25+) | No (third-party patch) |
| WebSockets | Yes | Yes (mod_proxy_wstunnel) |
| WordPress | Excellent (try_files) | Excellent (native .htaccess) |
| Kubernetes ingress | Default choice | Available (less common) |
| Community / docs | Large, active | Very large, 30 years of docs |
| License | BSD-2-Clause | Apache 2.0 |
Common mistakes
| Mistake | Why it's a problem | Fix |
|---|---|---|
Using if in Nginx location blocks |
if is "evil" in Nginx — unexpected behaviour with try_files |
Use try_files, map, or return instead |
Forgetting proxy_set_header Host |
Backend sees wrong hostname | Always set Host, X-Real-IP, X-Forwarded-For |
AllowOverride All on high-traffic Apache |
Re-reads .htaccess every request | Use AllowOverride None and move rules to VirtualHost |
| Mixing Nginx and Apache config syntax | They are not compatible | Keep configs in separate files, know which server you're editing |
| Serving PHP files through Nginx directly | Nginx can't execute PHP — it will serve PHP source code | Always proxy .php to PHP-FPM |
No worker_processes auto in Nginx |
Uses only 1 CPU core by default | Set worker_processes auto; in nginx.conf |
| Not reloading after config change | Old config stays active | Run nginx -s reload or apache2ctl graceful |
Hardcoding 127.0.0.1 in upstream |
Breaks in Docker/K8s | Use service names / environment variables |
FAQ
Which is faster, Nginx or Apache?
For static files and high concurrency, Nginx is faster. For dynamic content (PHP, Python), performance is similar once both use FPM/WSGI proxying. The difference matters most at scale — at low traffic, both are more than fast enough.
Can Nginx read .htaccess files?
No. Nginx intentionally does not support .htaccess. All config must be in the server config files. This is a feature, not a limitation — it avoids per-request disk reads and keeps config centralised.
Which should I use for WordPress?
If you control the server config, Nginx + PHP-FPM is the faster choice. If you're on shared hosting (cPanel), Apache with .htaccess is the practical choice. Both support WordPress fully.
Is Apache dead?
No. Apache still powers roughly 25% of the web and is actively developed. It's the better choice for shared hosting and legacy apps. The narrative that "Nginx replaced Apache" is overstated — they serve different niches.
What about Caddy, LiteSpeed, or Traefik?
Caddy auto-provisions TLS and has a simpler config — good for small teams. LiteSpeed is drop-in Apache compatible with better performance (used by LiteSpeed Web Host). Traefik excels as a Kubernetes-native reverse proxy/ingress. Nginx remains the most widely deployed in production.
How do I migrate from Apache to Nginx?
- Install Nginx alongside Apache (Apache on port 8080, Nginx on 80/443)
- Convert
.htaccessrules to Nginxlocationblocks (use online converters as a starting point) - Test thoroughly in staging
- Switch DNS / remove Apache once stable
The main challenge is translatingmod_rewriterules to Nginxrewrite/try_files/returndirectives.