caduh

Nginx vs Apache — a practical comparison and migration guide

5 min read

Event‑driven Nginx vs process/thread‑based Apache: performance, memory, config models, .htaccess, reverse proxying, PHP, HTTP/2/3, and how to switch without breaking prod.

TL;DR

  • Architecture: Nginx is event‑driven, async, non‑blocking → great at static files & reverse proxying with low memory at high concurrency. Apache uses MPMs (process/thread models: prefork/worker/event) → flexible, strong .htaccess and module ecosystem.
  • Performance: For many concurrent connections, Nginx typically uses less RAM and has lower overhead; Apache can be competitive with the event MPM and proper tuning.
  • Config model: Nginx has centralized config (nginx.conf, no per‑dir overrides). Apache supports per‑directory .htaccess, handy for shared hosting but slower and harder to audit.
  • Dynamic apps: Both commonly front PHP‑FPM, Node, Python, etc. Nginx proxies to app servers; Apache can do the same (or run modules like mod_php in legacy setups).
  • HTTP/2/3 & TLS: Both support HTTP/2 widely; HTTP/3/QUIC support exists but may require newer builds or modules depending on distro.
  • Choosing: Prefer Nginx as edge/reverse proxy and for high‑throughput static delivery. Prefer Apache if you rely on .htaccess, deep .htaccess‑driven rewrites, or existing Apache‑centric hosting. Many stacks run Nginx in front of Apache.

Architecture & mental model (60 seconds)

Nginx
  master ──> workers (event loop)
  • Non‑blocking sockets
  • Predictable RAM use as concurrency rises

Apache (choose an MPM)
  prefork: 1 process per request (no threads)
  worker:  processes with threads
  event:   like worker but keeps idle keep‑alives off threads
  • Nginx shines as a reverse proxy, static file server, and TLS terminator.
  • Apache offers a rich module system and per‑dir config with .htaccess (shared hosting, CMS rewrites).

Practical comparison

| Topic | Nginx | Apache | |---|---|---| | Concurrency model | Event‑driven workers | MPMs (process/thread/event) | | Memory use at scale | Low per connection | Higher with many processes/threads (tunable) | | Static file throughput | Excellent | Good; can be strong with event MPM | | Reverse proxy | First‑class (proxy_pass, upstream) | First‑class (mod_proxy, mod_proxy_balancer) | | Per‑directory overrides | ❌ None | ✅ .htaccess | | Config style | Declarative, centralized | Hierarchical; per‑vhost + optional .htaccess | | PHP | Via php‑fpm (fastcgi) | Via php‑fpm (proxy_fcgi) or legacy mod_php | | HTTP/2 | ✅ | ✅ (mod_http2) | | HTTP/3 (QUIC) | Available in newer builds | Available via newer modules/builds (varies by distro) | | Windows support | Limited/less common in prod | More common than Nginx but still Linux favored |


Minimal configs (side‑by‑side)

Nginx — static + reverse proxy to app on :3000

server {
  listen 80;
  server_name example.com;

  # Static
  root /var/www/html;
  location /assets/ { try_files $uri =404; }

  # Security headers (sample)
  add_header X-Content-Type-Options nosniff;
  add_header Referrer-Policy same-origin;

  # Reverse proxy
  location /api/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

Apache — equivalent vhost (proxy to :3000)

<VirtualHost *:80>
  ServerName example.com
  DocumentRoot /var/www/html

  # Static
  <Directory /var/www/html>
    Options FollowSymLinks
    AllowOverride None
    Require all granted
  </Directory>

  # Security headers (sample)
  Header always set X-Content-Type-Options "nosniff"
  Header always set Referrer-Policy "same-origin"

  # Reverse proxy
  ProxyPreserveHost On
  ProxyPass        /api/ http://127.0.0.1:3000/
  ProxyPassReverse /api/ http://127.0.0.1:3000/
</VirtualHost>
# Required modules (enable on Debian/Ubuntu):
# a2enmod headers proxy proxy_http

PHP‑FPM fast‑path

Nginx

location ~ \.php$ {
  include fastcgi_params;
  fastcgi_pass unix:/run/php/php-fpm.sock;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Apache (proxy_fcgi)

<FilesMatch \.php$>
  SetHandler "proxy:unix:/run/php/php-fpm.sock|fcgi://localhost/"
</FilesMatch>

Performance notes (real‑world)

  • Keep‑alive & H2: Nginx’s event loop handles many idle connections efficiently (think SPA + H2 multiplexing). Apache’s event MPM narrows the gap—use it if high concurrency matters.
  • Static assets: Nginx often leads; or place a CDN/edge cache in front of either server.
  • App backends: Both are usually not the bottleneck if you’re proxying to Node/Go/Python—network and app performance dominate. Measure before migrating.

Config model trade‑offs

  • Nginx (centralized): predictable and fast. Changes require editing vhost files and reloading.
  • Apache (.htaccess): allows unprivileged per‑site tweaks (shared hosting), but every request may trigger per‑dir lookup/parsing. Disable when you don’t need it: AllowOverride None.

TLS, H2/H3 quick sketch

  • Enable HTTP/2 on both; ensure ALPN via modern TLS stacks.
  • HTTP/3/QUIC support is available in current ecosystems but may need specific packages or flags; test behind your load balancer/CDN first.

Observability & ops

  • Logs: both support JSON/custom formats; ship via syslog or agents.
  • Reloads: Nginx supports zero‑downtime config reloads (nginx -s reload). Apache supports graceful reloads (apachectl graceful).
  • Rate limiting / WAF: Nginx has limit_req, limit_conn; Apache has mod_evasive, mod_security on both sides (WAF often at a gateway/CDN).

Migration paths

A) Nginx → Apache

  • Translate server blocks to <VirtualHost>; replace proxy_set_header with ProxyPreserveHost + headers.
  • If you relied on Nginx rewrites, port to mod_rewrite or app routes.
  • Enable event MPM for concurrency; disable .htaccess unless needed.

B) Apache → Nginx

  • Convert .htaccess rules to Nginx location/rewrite (many CMS have guides).
  • Replace ProxyPass with proxy_pass and upstreams; test header parity (X-Forwarded-*).
  • Move PHP to php‑fpm and verify uploaded file limits and timeouts.
  • Use nginx -T/-s reload and stage behind a temporary port or blue/green cutover.

C) Layered: Nginx in front of Apache

  • Keep Apache for complex .htaccess/rewrite‑heavy apps; let Nginx handle TLS, H2/H3, compression, and static assets; proxy dynamic requests to Apache.

Pitfalls & fixes

| Problem | Why it happens | Fix | |---|---|---| | .htaccess rules don’t work on Nginx | Nginx doesn’t read .htaccess | Convert to Nginx directives; remove per‑dir overrides | | High RAM with Apache under load | Process/thread per connection | Use event MPM, tune MaxRequestWorkers, keep‑alive timeouts | | 502 Bad Gateway after swap | Backend headers/timeouts differ | Align proxy headers; increase proxy_read_timeout/ProxyTimeout | | File permission/SELinux denials | Different user contexts | Align user/group; add SELinux labels (:Z), check logs | | HTTP/3 not working | Build/stack mismatch | Verify packages and LB/CDN support; fall back to H2 while testing |


Quick checklist

  • [ ] Decide on centralized config (Nginx) vs per‑dir overrides (Apache).
  • [ ] Enable HTTP/2; test HTTP/3 only after confirming platform support.
  • [ ] If using PHP, standardize on php‑fpm with proper socket/timeout tuning.
  • [ ] For high concurrency, use Nginx or Apache event MPM; set keep‑alive sensibly.
  • [ ] Mirror proxy headers (X‑Forwarded‑*) and timeouts across environments.
  • [ ] Plan a blue/green or canary migration with health checks and fast rollback.

One‑minute decision guide

  • Need max throughput as an edge proxy/static server with simple, centralized config? → Nginx.
  • Need per‑site overrides, legacy CMS rewrites, or shared hosting ergonomics? → Apache.
  • Can’t choose? → Put Nginx in front of Apache; move complexity inward over time.