A deep technical comparison of two web servers that have coexisted for 30 years — and why choosing between them is less about speed and more about your architecture.
Here's the conversation that plays out constantly in engineering teams: someone proposes switching from Apache to Nginx (or vice versa) because of a benchmark they read. The benchmark shows one server handling 10,000 concurrent connections versus the other's 3,000. The team switches. Six months later, performance is largely unchanged, but now the .htaccess-dependent deployment workflow is broken, PHP-FPM configuration has to be rebuilt from scratch, and three WordPress plugins have started behaving strangely.
The benchmark wasn't wrong. The decision was.
Apache and Nginx solve genuinely different problems, make different architectural tradeoffs, and excel in different deployment contexts. The question isn't which one is faster in a synthetic test — it's which one fits your application's actual request pattern, your team's operational knowledge, and your infrastructure constraints.
This article is a technical deep-dive into both servers: how they actually handle connections, what their configuration models mean for real deployments, where each one is definitively better, and how to run them together when neither is sufficient alone.
The Fundamental Architectural Difference
Everything that matters in the Apache vs Nginx comparison flows from one architectural decision made in each server's design.
Apache: Process/Thread per Connection
Apache's default Multi-Processing Module (MPM) — prefork — spawns a new process for every incoming connection. Each process handles exactly one request at a time. worker MPM uses threads instead of processes, but the principle is the same: one thread per concurrent connection.
Apache (prefork MPM):
Connection 1 → [Process 1: handling request] → Response
Connection 2 → [Process 2: handling request] → Response
Connection 3 → [Process 3: waiting for DB] ← blocked
Connection 4 → [Process 4: waiting for DB] ← blocked
...
Connection N → [Process N: spawning...] ← resource pressure
Under high concurrency, this model has a hard ceiling: each idle process waiting on I/O (database query, upstream API call, slow client upload) still consumes memory and a file descriptor. A server with 512MB available for Apache workers and 30MB per process can sustain roughly 17 simultaneous connections before it starts swapping.
Nginx: Event-Driven, Asynchronous, Non-Blocking
Nginx uses an event loop architecture. A small, fixed number of worker processes handle all connections through non-blocking I/O and an event notification system (epoll on Linux, kqueue on BSD). A worker is never blocked waiting for a slow client or upstream — it registers interest in an event, handles other connections, and resumes when the event fires.
Nginx (event-driven):
Worker Process 1:
├── Connection 1: serving static file (reading disk)
├── Connection 2: proxying to upstream (waiting for response)
├── Connection 3: sending response (writing to socket)
├── Connection 4: reading request body
└── ... (thousands more)
Worker Process 2:
└── (same pattern)
Total workers = number of CPU cores
The consequence: Nginx handles 10,000 simultaneous connections with the same memory footprint as it uses for 100. Apache's memory usage grows linearly with concurrent connections.
When This Distinction Actually Matters
The architectural difference only produces measurable real-world impact under specific conditions:
High concurrency with slow upstreams — If your PHP, Python, or Node.js backend takes 500ms to respond and you have 1,000 concurrent users, Apache holds 1,000 processes in memory doing nothing but waiting. Nginx holds 1,000 event registrations in a single worker's event loop — negligible overhead.
Static file serving at scale — Nginx was built for this. Serving large numbers of concurrent file downloads or CDN-origin requests is where the event model wins decisively.
Long-polling, SSE, WebSockets — Connections that stay open for seconds or minutes are expensive under Apache's process model. Under Nginx's event model, they're nearly free in terms of memory.
For a typical CRUD web application with moderate traffic (< 500 concurrent users) on adequate hardware, the architectural difference produces no perceptible performance difference. The decision should be driven by other factors.
Configuration Philosophy: .htaccess vs Server Blocks
This is where teams feel the difference the most day-to-day.
Apache: Distributed, Runtime Configuration
Apache allows configuration to be distributed into .htaccess files placed in any directory. These files are read on every request — Apache checks for .htaccess in every directory component of the request path.
# /var/www/html/app/.htaccess
Options -Indexes
RewriteEngine On
RewriteBase /app/
# WordPress-style pretty URLs
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /app/index.php [L]
# Custom headers per directory
Header always set X-Content-Type-Options nosniff
# Directory-level auth
AuthType Basic
AuthName "Staging Environment"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
The power of .htaccess is that it can be modified without server restart and without access to the main server configuration. Shared hosting providers use this to give each user control over their own document root without granting server-wide config access. WordPress, Drupal, and many PHP frameworks depend on it for URL rewriting.
The cost: every request causes Apache to read multiple .htaccess files from disk. On a filesystem with many directory levels, this adds measurable I/O overhead. The performance impact is real but often tolerable; the bigger problem is that .htaccess makes configuration harder to audit — effective site behavior is scattered across an arbitrary number of files.
Disabling .htaccess for performance (when you control all config):
# httpd.conf or site config
<Directory /var/www/html>
AllowOverride None # Disables .htaccess lookup entirely
Options -Indexes
</Directory>
Nginx: Centralized, Static Configuration
Nginx has no equivalent of .htaccess. All configuration lives in server-level config files, read at startup. Changes require a reload (nginx -s reload or systemctl reload nginx).
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com www.example.com;
root /var/www/html;
# Equivalent of WordPress .htaccess rewrite
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
# Cache static assets at the edge
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
The absence of .htaccess is not a limitation — it's an architectural choice that makes configuration auditable, version-controllable, and faster to execute. The tradeoff is operational: any configuration change requires access to server-level files and a reload.
For teams managing their own infrastructure, this is almost always preferable. For shared hosting scenarios where individual application owners need runtime config control, Apache's model is unavoidable.
PHP Execution: Where the Models Diverge Most
How each server executes PHP reveals a fundamental difference in their extension models.
Apache + mod_php: PHP Inside the Server Process
Apache's mod_php module embeds the PHP interpreter directly into each Apache worker process. When a .php request arrives, the same process that handles HTTP also executes PHP.
# Apache with mod_php
LoadModule php_module modules/libphp8.so
<FilesMatch \.php$>
SetHandler application/x-httpd-php
</FilesMatch>
Consequences of this model:
Every Apache worker process — whether currently serving a PHP request or a CSS file — has the PHP interpreter loaded in memory. A server running 50 Apache workers with mod_php might use 150MB on PHP interpreter overhead alone, even if only 5 of those workers are executing PHP at any given moment.
mod_php also runs PHP as the Apache user (www-data), which limits multi-tenant isolation and makes user-specific PHP configurations (different PHP versions per virtual host) difficult.
Nginx + PHP-FPM: Separation of Concerns
Nginx has no built-in PHP module. PHP execution is delegated to PHP-FPM (FastCGI Process Manager) — a separate process pool that Nginx communicates with via Unix socket or TCP.
# Nginx delegates PHP to PHP-FPM via Unix socket
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Performance tuning
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
fastcgi_read_timeout 300;
}
PHP-FPM pool configuration:
; /etc/php/8.3/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
; Dynamic pool: spawns workers on demand
pm = dynamic
pm.max_children = 25
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500
; Per-pool PHP settings
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 64M
php_flag[display_errors] = off
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
The operational advantages of PHP-FPM:
- PHP workers only consume memory when executing PHP — Nginx workers serving static files carry no PHP overhead
- Different virtual hosts can use different PHP versions (separate FPM pools per site)
- PHP-FPM can run as a different user per pool, providing process-level isolation between sites
- PHP-FPM can be restarted independently of the web server
- OPcache is shared across all FPM workers in a pool, reducing memory usage
Apache can also use PHP-FPM (via mod_proxy_fcgi), and this configuration is increasingly common — but Nginx's configuration for PHP-FPM is simpler and requires fewer modules.
Reverse Proxy and Load Balancing
Both servers can function as reverse proxies, but their capabilities differ in meaningful ways.
Nginx as a Reverse Proxy
Nginx was designed from the ground up with proxying as a first-class use case. Its event-driven model makes it particularly efficient at maintaining many simultaneous upstream connections.
# Upstream pool with load balancing
upstream app_backend {
least_conn; # Route to server with fewest active connections
server 10.0.0.1:3000 weight=3;
server 10.0.0.2:3000 weight=3;
server 10.0.0.3:3000 weight=1 backup; # Only used if others fail
keepalive 32; # Keep 32 connections open per worker to upstream
}
server {
listen 443 ssl http2;
location /api/ {
proxy_pass http://app_backend;
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;
# Buffering: read full upstream response before sending to client
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 16k;
# Timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Health-based failover
proxy_next_upstream error timeout http_500 http_502 http_503;
proxy_next_upstream_tries 2;
}
}
Apache as a Reverse Proxy
Apache's mod_proxy and mod_proxy_balancer provide reverse proxy functionality, but with more configuration verbosity and less efficient connection handling under high concurrency.
# Apache reverse proxy with load balancing
<Proxy balancer://app_backend>
BalancerMember http://10.0.0.1:3000 loadfactor=3
BalancerMember http://10.0.0.2:3000 loadfactor=3
BalancerMember http://10.0.0.3:3000 loadfactor=1 status=+H
ProxySet lbmethod=byrequests
</Proxy>
<VirtualHost *:443>
ProxyPreserveHost On
ProxyPass /api/ balancer://app_backend/
ProxyPassReverse /api/ balancer://app_backend/
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
</VirtualHost>
For reverse proxy workloads specifically, Nginx is the more commonly chosen option — its upstream keepalive behavior, buffer tuning, and health check configuration are more granular and better documented for production use.
The Combined Architecture: Nginx in Front of Apache
One deployment pattern that appears frequently in production — particularly in WordPress hosting and mature PHP application stacks — runs Nginx as a front-end reverse proxy in front of Apache.
Internet → [Nginx: port 80/443]
|
┌──────────┼──────────┐
↓ ↓ ↓
Static files [Apache: port 8080] Cache layer
(served by |
Nginx directly) PHP via mod_php
(or PHP-FPM)
This pattern captures the benefits of both: Nginx handles static file serving and TLS termination with its event-driven efficiency, while Apache handles PHP execution with mod_php (for .htaccess-dependent applications) or PHP-FPM on the backend. The development team keeps their existing .htaccess workflow; the infrastructure gets Nginx's concurrency handling at the edge.
# Nginx front-end: serve static files directly, proxy PHP to Apache
server {
listen 443 ssl http2;
server_name example.com;
root /var/www/html;
# Serve static files directly from Nginx — never hits Apache
location ~* \.(css|js|jpg|jpeg|png|gif|ico|woff|woff2|svg|pdf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Send everything else to Apache
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 https;
}
}
# Apache back-end: listen on 8080, handle PHP
Listen 8080
<VirtualHost *:8080>
DocumentRoot /var/www/html
ServerName example.com
# .htaccess still works for application logic
<Directory /var/www/html>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
This is particularly relevant for teams handling legacy PHP applications where a full Nginx migration would require rewriting extensive .htaccess rule sets.
Modules and Extensibility
Apache's Module System
Apache's module system is one of its most powerful features. Modules can be dynamically loaded at runtime and can hook into the request processing pipeline at any stage.
# Load modules dynamically
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule headers_module modules/mod_headers.so
LoadModule expires_module modules/mod_expires.so
LoadModule deflate_module modules/mod_deflate.so
LoadModule ssl_module modules/mod_ssl.so
# Check available modules
apache2ctl -M
# Enable/disable on Debian/Ubuntu
a2enmod rewrite headers expires deflate
a2dismod status
Notable Apache-exclusive capabilities:
-
mod_security— the most mature open-source WAF (Web Application Firewall), deeply integrated into Apache's request pipeline -
mod_evasive— DDoS mitigation at the web server level -
mod_pagespeed— Google's automatic performance optimization module (rewrites HTML, CSS, JS in-flight) -
mod_perl/mod_python— embed interpreter directly in Apache worker (analogous to mod_php)
Nginx's Module System
Nginx modules must be compiled into the binary — there's no runtime dynamic loading in the open-source version (Nginx Plus supports dynamic modules). Third-party modules require recompiling from source.
# Check compiled modules
nginx -V 2>&1 | tr -- - '\n' | grep module
# Compile Nginx with additional modules
./configure \
--prefix=/etc/nginx \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--add-module=/path/to/nginx-module-vts
make && sudo make install
The Nginx package from nginx.org includes a standard set of modules. Debian/Ubuntu's nginx-extras package includes additional third-party modules (GeoIP, Passenger, RTMP) precompiled.
Nginx's module ecosystem is narrower than Apache's but covers the core use cases well. For WAF functionality, ModSecurity now has an Nginx connector, though the integration is less seamless than the native Apache version.
Security Configuration Comparison
Hardening Apache
# /etc/apache2/conf-available/security.conf
# Hide Apache version and OS from error pages and headers
ServerTokens Prod
ServerSignature Off
# Disable directory browsing globally
Options -Indexes
# Prevent access to .htaccess and .htpasswd files
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
# Disable TRACE method (XST attack vector)
TraceEnable Off
# Clickjacking protection
Header always set X-Frame-Options DENY
# XSS protection
Header always set X-Content-Type-Options nosniff
Header always set X-XSS-Protection "1; mode=block"
# HSTS
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# Content Security Policy
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'"
Hardening Nginx
# /etc/nginx/nginx.conf — http block
# Hide Nginx version
server_tokens off;
# Limit request methods
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|PATCH)$) {
return 444; # Close connection without response
}
# Limit request size
client_max_body_size 10m;
client_body_timeout 12;
client_header_timeout 12;
# Buffer overflow protections
client_body_buffer_size 1K;
client_header_buffer_size 1k;
large_client_header_buffers 2 1k;
# server block
server {
# Security headers
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=()" always;
# Block common exploit patterns
location ~* (eval\(|base64_decode|GLOBALS|_REQUEST) {
return 403;
}
# Deny access to sensitive files
location ~* \.(env|git|svn|htaccess|htpasswd|ini|log|sh|sql|bak)$ {
return 403;
}
}
Performance Benchmarks: Reading Them Correctly
Benchmarks comparing Apache and Nginx almost always test static file serving under high concurrency — because that's the easiest thing to benchmark cleanly. Under those conditions, Nginx wins clearly and consistently.
But real web applications are not static file servers under synthetic load. Here's a more representative breakdown:
| Workload | Apache | Nginx | Notes |
|---|---|---|---|
| Static files, high concurrency | ◯ Adequate | ● Better | Event model wins above ~500 concurrent |
| PHP (via PHP-FPM) | ● Equivalent | ● Equivalent | FPM performance is backend-bound, not server-bound |
| PHP (via mod_php) | ● Simpler | ✗ N/A | Apache-only feature |
| Reverse proxy, high concurrency | ◯ Adequate | ● Better | Upstream keepalive tuning is more effective in Nginx |
| WebSocket / SSE (long connections) | ✗ Expensive | ● Efficient | Process per connection becomes critical at scale |
.htaccess support |
● Native | ✗ None | Nginx has no equivalent |
| Dynamic configuration (no reload) | ● Via .htaccess | ✗ Requires reload | Matters for shared hosting, not dedicated servers |
| Module ecosystem | ● Broader | ◯ Adequate for most | Apache has more specialized modules |
| Configuration learning curve | ◯ Familiar | ● Cleaner syntax | Nginx directives are more intuitive for new configs |
Real-World Deployment Context
The practical choice between Apache and Nginx often depends less on raw performance and more on the operational context in which a site is deployed.
Teams involved in website development in Huntley Illinois frequently work with clients on shared hosting environments where Apache is the only available option — and in that context, the .htaccess model isn't a limitation, it's the mechanism that makes application-level URL rewriting and access control possible without server-admin privileges. Optimizing Apache on shared hosting means disabling unnecessary modules, tuning AllowOverride to the minimum needed, and enabling mod_deflate and mod_expires for compression and browser caching.
For developers doing website development in Wilmette Illinois on managed VPS infrastructure — where full server access is available and the deployment targets custom Node.js or Python backends — Nginx is almost always the better operational fit. Its configuration model maps cleanly to modern deployment patterns: TLS termination at the edge, upstream proxying to application servers, static asset serving with long cache TTLs, and HTTP/2 push. The absence of .htaccess is irrelevant when the application handles its own routing.
Both observations point to the same conclusion: the server choice is an infrastructure decision, not a performance religion.
The Decision Framework
Instead of a blanket recommendation, here's a practical decision tree:
Choose Apache if:
- You're on shared hosting (often no choice)
- Your application depends on
.htaccessrules you can't or don't want to rewrite - You need
mod_securityWAF with native integration - Your team has deep existing Apache operational knowledge
- You're running a legacy PHP application where mod_php is the path of least resistance
Choose Nginx if:
- You're managing your own VPS or dedicated server
- You're building a reverse proxy or load balancer layer
- You need efficient handling of WebSockets, SSE, or long-polling connections
- You're proxying to Node.js, Python, Go, or any non-PHP backend
- You value centralized, auditable, version-controlled server configuration
- You're deploying at scale where memory efficiency under concurrency matters
Use both (Nginx → Apache) if:
- You have a legacy Apache/mod_php application you can't migrate immediately
- You need
.htaccessfor application-level config but want Nginx's front-end efficiency - You're migrating gradually from Apache to a full Nginx stack
Quick Configuration Reference
Virtual Host: Apache
# /etc/apache2/sites-available/example.com.conf
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com/public
<Directory /var/www/example.com/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/example.com-error.log
CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined
# Redirect to HTTPS
Redirect permanent / https://example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/example.com/public
SSLEngine on
SSLCertificateFile /etc/ssl/certs/example.com.crt
SSLCertificateKeyFile /etc/ssl/private/example.com.key
SSLCertificateChainFile /etc/ssl/certs/intermediate.crt
</VirtualHost>
# Enable site and reload
a2ensite example.com
systemctl reload apache2
Server Block: Nginx
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
root /var/www/example.com/public;
ssl_certificate /etc/ssl/certs/example.com.fullchain.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
access_log /var/log/nginx/example.com-access.log;
error_log /var/log/nginx/example.com-error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
# Enable site and reload
ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
Final Thought
Apache and Nginx are not competitors in the way that framework choices are competitors. They represent two different design philosophies for handling concurrent network connections — one process/thread-based and one event-driven — and those philosophies produce different operational tradeoffs that matter in different contexts.
The developers who make the best infrastructure decisions aren't the ones who have a strong opinion about which server is better. They're the ones who understand why each server behaves the way it does, so they can predict how each will behave under their specific workload, with their specific application stack, at their specific scale.
The answer to "Apache or Nginx?" is almost always "it depends" — but if you understand the architectural differences described here, "it depends on what" becomes a much easier question to answer.
Top comments (0)