Nginx and Apache have dominated web server deployments for over two decades. Both are production-proven, both are open-source, and both will serve your application — but they make fundamentally different tradeoffs in how they handle connections, memory, and configuration. Understanding those tradeoffs is what separates a good infrastructure decision from a painful one that costs you months of rework.
Architecture: Event-Driven vs Process-Based
The most important difference between Nginx and Apache is the concurrency model. Everything else — performance characteristics, memory usage, operational behavior under load — flows directly from this architectural choice.
Apache's Multi-Processing Modules
Apache uses a pluggable Multi-Processing Module (MPM) system. The three MPMs you will encounter in production are:
- prefork MPM: Spawns a dedicated process for each connection. Crash isolation is excellent — a broken process cannot corrupt others — but memory consumption is severe. Each child process typically consumes 10–30 MB of RAM, making it impractical at high concurrency.
- worker MPM: Uses a hybrid model with multiple threads per process. More efficient than prefork, but historically incompatible with non-thread-safe PHP extensions, which forced many shops to stay on prefork for years.
- event MPM: The modern default on Apache 2.4+. Offloads idle keep-alive connections to a dedicated listener thread, freeing worker threads for active request processing. Substantially more efficient than prefork, but still fundamentally thread-per-active-request under the hood.
Even with the event MPM, Apache binds a thread to an active connection for the duration of request processing. Under sustained high concurrency, this architectural ceiling becomes measurable — and expensive.
Nginx's Event-Driven, Non-Blocking Model
Nginx was designed from scratch to solve the C10K problem — serving 10,000 concurrent connections on commodity hardware. It uses an asynchronous, non-blocking event loop powered by OS-level primitives: epoll on Linux, kqueue on BSD systems. A single Nginx worker process multiplexes thousands of connections without spawning additional threads or processes.
Worker count is matched to available CPU cores, and connection capacity scales accordingly:
worker_processes auto;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
# On sw-infrarunbook-01 (4-core): 4 workers x 4096 = 16,384 max connections
# All handled within the existing worker processes
Apache would need to spawn thousands of threads to match that concurrency — often exhausting available RAM before reaching the connection limit. Nginx handles it entirely within its existing worker footprint.
Performance: Static Files, Dynamic Content, and Memory
Static File Serving
Nginx consistently outperforms Apache for static asset delivery. Across benchmark configurations, Nginx delivers 2–4x more requests per second for static files under equivalent concurrency. The event-driven model eliminates per-connection thread context-switch overhead that Apache's MPM architecture inherently carries.
A tuned static file configuration for sw-infrarunbook-01 serving assets for solvethenetwork.com:
server {
listen 443 ssl;
server_name solvethenetwork.com;
root /var/www/solvethenetwork/public;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding";
access_log off;
gzip_static on;
}
}
Dynamic Content: mod_php vs PHP-FPM
This is where the comparison becomes nuanced. Apache's mod_php embeds the PHP interpreter directly into each worker process. There is no inter-process communication overhead for PHP execution — the runtime is already loaded in memory when the request arrives. For single-threaded request latency, mod_php can be faster than a FastCGI roundtrip.
Nginx has no equivalent. It always proxies dynamic content to an external FastCGI, uWSGI, or reverse proxy backend. For PHP, that means PHP-FPM via a Unix socket:
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
include fastcgi_params;
fastcgi_read_timeout 30;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_intercept_errors on;
}
Under high concurrency, Nginx + PHP-FPM wins decisively. PHP workers are decoupled from connection handling — a slow PHP script does not block the web server from accepting new connections. Apache with mod_php ties a thread to the PHP execution for its entire duration, which caps throughput when PHP workers are saturated.
Memory Footprint
On sw-infrarunbook-01 (8 GB RAM, 4 cores) handling 500 concurrent connections:
- Apache prefork: ~500 child processes × 15 MB average = approximately 7.5 GB RAM consumed by the web server alone
- Apache event MPM: Substantially better — roughly 50 active threads plus idle connection bookkeeping, typically 100–300 MB total
- Nginx: 4 worker processes × 4–6 MB = approximately 20–25 MB for connection management; PHP-FPM worker pool is sized and managed separately
For containerized deployments where memory limits are strict, Nginx's footprint advantage is decisive. A PHP-FPM container paired with Nginx uses a fraction of the RAM an equivalent Apache prefork deployment would require.
Configuration: Centralized vs Distributed
Apache's .htaccess System
Apache supports per-directory .htaccess files that override server configuration without requiring a server reload. This enables application-level URL rewriting, authentication, and access control rules to be shipped inside the application directory itself:
# /var/www/solvethenetwork/public/.htaccess
Options -Indexes +FollowSymLinks
AllowOverride All
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Laravel / Symfony front controller
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
The tradeoff is performance. Apache must perform a stat() filesystem call for every possible .htaccess file location on every request, traversing the entire directory tree from document root to the requested file. On a high-request-rate server, this overhead is measurable. Benchmarks show .htaccess lookups adding 1–5% overhead on static file requests and more on deeply nested paths.
Nginx's Centralized Configuration
Nginx has no .htaccess equivalent — by deliberate design. All configuration lives in the main hierarchy under
/etc/nginx/. Changes require a reload (
nginx -s reload), but zero per-request filesystem overhead is incurred for configuration lookups:
# /etc/nginx/sites-available/solvethenetwork.com
server {
listen 80;
server_name solvethenetwork.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name solvethenetwork.com;
root /var/www/solvethenetwork/public;
ssl_certificate /etc/ssl/certs/solvethenetwork_chain.crt;
ssl_certificate_key /etc/ssl/private/solvethenetwork.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
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;
}
}
Nginx's block-based
location {}matching is powerful but has a learning curve — particularly around prefix vs regex matching priority. The rule: exact matches (
=) take highest priority, then regex (
~and
~*), then longest prefix match. Misunderstanding this order is the most common source of Nginx configuration bugs.
Reverse Proxy and Load Balancing
For reverse proxy workloads, Nginx is widely considered the industry standard. Its upstream block system provides flexible, high-performance load balancing with minimal configuration overhead.
Nginx Upstream Configuration
upstream app_cluster {
least_conn;
server 10.10.20.11:8080 weight=3 max_fails=3 fail_timeout=30s;
server 10.10.20.12:8080 weight=3 max_fails=3 fail_timeout=30s;
server 10.10.20.13:8080 backup;
keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}
server {
listen 443 ssl;
server_name solvethenetwork.com;
location /api/ {
proxy_pass http://app_cluster;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
}
The
keepalivedirective in the upstream block enables HTTP/1.1 persistent connections to backend servers — eliminating TCP handshake overhead on every proxied request. For a high-throughput API routing to 10.10.20.11, 10.10.20.12, and 10.10.20.13, upstream keepalives alone can reduce end-to-end latency by 10–30% under sustained traffic. Setting
proxy_http_version 1.1and clearing the
Connectionheader is required to activate keepalive pooling to backends.
Apache mod_proxy_balancer
# /etc/httpd/conf.d/balancer.conf
<Proxy balancer://app_cluster>
BalancerMember http://10.10.20.11:8080 loadfactor=3
BalancerMember http://10.10.20.12:8080 loadfactor=3
BalancerMember http://10.10.20.13:8080 status=+H
ProxySet lbmethod=byrequests
ProxySet nofailover=Off
</Proxy>
<VirtualHost 10.10.10.15:443>
ServerName solvethenetwork.com
ProxyPass /api/ balancer://app_cluster/
ProxyPassReverse /api/ balancer://app_cluster/
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
</VirtualHost>
Apache's balancer module is functional but lacks the fine-grained upstream connection pooling that Nginx provides natively. For most high-throughput API gateway use cases, Nginx is the stronger choice.
Module Systems
Apache Dynamic Shared Objects (DSO)
Apache modules can be loaded and unloaded without recompiling the server binary. This makes module management straightforward on production systems:
# /etc/httpd/conf/httpd.conf
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule security2_module modules/mod_security2.so
LoadModule headers_module modules/mod_headers.so
LoadModule deflate_module modules/mod_deflate.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
Key Apache modules: mod_rewrite (URL rewriting), mod_security (WAF), mod_ssl (TLS), mod_deflate (gzip), mod_auth_mellon (SAML), mod_auth_kerb (Kerberos), mod_headers (header manipulation).
Nginx Modules: Compiled-In and Dynamic
Most Nginx modules are compiled at build time. Verify what is available on sw-infrarunbook-01 before assuming a feature is present:
nginx -V 2>&1 | tr -- - '\n' | grep "^with-"
# Representative output:
# with-http_ssl_module
# with-http_v2_module
# with-http_realip_module
# with-http_addition_module
# with-http_gzip_static_module
# with-http_stub_status_module
# with-stream
# with-stream_ssl_module
Since Nginx 1.9.11, dynamic modules (.so files) are supported for official modules. Third-party modules — Lua scripting via OpenResty, ModSecurity WAF, RTMP streaming — typically require custom builds or the OpenResty distribution. This is a genuine operational consideration: adding a module to Nginx often means recompiling or replacing the binary, whereas Apache simply requires loading a new .so.
SSL/TLS Hardening
Both servers support modern TLS configurations, but Nginx's SSL termination is more commonly deployed at scale due to its lower per-connection overhead. A hardened TLS configuration for sw-infrarunbook-01 acting as the SSL terminator for solvethenetwork.com:
ssl_certificate /etc/ssl/certs/solvethenetwork_chain.crt;
ssl_certificate_key /etc/ssl/private/solvethenetwork.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:20m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/solvethenetwork_ca.crt;
resolver 10.10.1.1 10.10.1.2 valid=300s;
resolver_timeout 5s;
Apache's equivalent requires mod_ssl and achieves the same cipher suite configuration, but TLS session cache and OCSP stapling require more directives and the implementation has historically been less performant than Nginx at high TLS connection rates.
Logging and Observability
Nginx Upstream Timing Logs
Adding upstream timing variables to Nginx logs is essential for diagnosing proxy performance on production systems. These variables are unique to Nginx and have no equivalent in Apache's native logging:
log_format upstream_perf '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time '
'uct=$upstream_connect_time '
'uht=$upstream_header_time '
'urt=$upstream_response_time '
'cs=$upstream_cache_status '
'ua="$upstream_addr"';
access_log /var/log/nginx/solvethenetwork_access.log upstream_perf;
error_log /var/log/nginx/solvethenetwork_error.log warn;
Apache Extended Log Format
LogFormat "%h %l %u %t %r %>s %b %D %{X-Forwarded-For}i %{User-Agent}i" extended
CustomLog /var/log/httpd/solvethenetwork_access.log extended
ErrorLog /var/log/httpd/solvethenetwork_error.log
LogLevel warn
# %D = total request time in microseconds (equivalent to Nginx $request_time)
# Apache has no native equivalent to $upstream_response_time
Apache's
%Dcaptures total request time but lacks visibility into upstream latency. Debugging whether slowness originates at the web server or a backend requires additional tooling (mod_log_forensic, custom middleware) that Nginx provides natively through its upstream variable set.
Security Headers
Nginx Security Header Configuration
server {
server_tokens off;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; font-src 'self'" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}
Apache Security Header Configuration
ServerTokens Prod
ServerSignature Off
TraceEnable Off
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set Content-Security-Policy "default-src 'self'"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Options -ExecCGI -Includes -Indexes
Hybrid Architecture: Running Both
Many mature production environments run Nginx in front of Apache. Nginx handles SSL termination, static assets, HTTP/2, and connection management. Apache handles legacy PHP applications that depend on .htaccess configuration, bound to the loopback interface and never exposed directly to the internet:
# Nginx on sw-infrarunbook-01: forward legacy app requests to local Apache
location /legacy-app/ {
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;
}
# Apache bound to loopback only — never receives direct internet connections
Listen 127.0.0.1:8080
<VirtualHost 127.0.0.1:8080>
ServerName solvethenetwork.com
DocumentRoot /var/www/legacy-app
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 127.0.0.1
<Directory /var/www/legacy-app>
AllowOverride All
Options -Indexes +FollowSymLinks
Require all granted
</Directory>
</VirtualHost>
This pattern lets infrastructure teams migrate incrementally, keeping .htaccess-dependent applications functional while new services are deployed behind Nginx with modern configuration.
Decision Guide: When to Choose Each Server
Choose Nginx when:
- You need a high-performance reverse proxy, API gateway, or load balancer
- You are serving large volumes of static content or media assets
- Memory efficiency is critical — containers, cloud VMs with cost-based sizing, or high-density bare-metal
- Your backend is Node.js, Go, Python (WSGI/ASGI), or Ruby
- You require WebSocket proxying at scale
- You are building microservices or service mesh ingress infrastructure
- You need mature HTTP/2 and HTTP/3 (QUIC) support
Choose Apache when:
- You need per-directory .htaccess configuration without server reloads — shared hosting, application-managed rewrites
- Legacy PHP applications depend on mod_php and cannot be ported to PHP-FPM
- You require mature enterprise authentication modules: mod_auth_mellon (SAML), mod_auth_kerb (Kerberos), mod_auth_gssapi
- Your team has deep Apache expertise and an existing configuration library worth preserving
- You are running a traditional shared hosting or cPanel environment
For new infrastructure projects without legacy constraints, Nginx is the better default. Its event-driven architecture, memory efficiency, first-class proxy capabilities, and operational simplicity make it the right choice for modern web infrastructure. Apache remains the right answer for specific, well-defined scenarios — it has not been made obsolete, it has been outpaced for general-purpose use cases.
