You already run Nginx in front of Apache. Port 80 and 443 hit Nginx, it proxies back to Apache on 8080 or 127.0.0.1, and you call it done. That setup works, but it leaves performance and reliability on the table. Below are eight advanced techniques I've seen work across high-traffic cPanel servers, custom VPS stacks, and WordPress farms—moves that separate a functional proxy from a genuinely fast, resilient one.
1. Enable upstream keepalive connections
By default, Nginx opens a new TCP connection to Apache for every request, then closes it. Apache pays the handshake cost every time, and under load that churn burns CPU and file descriptors.
Define an upstream block with keepalive enabled:
upstream apache_backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://apache_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
The keepalive 32; directive tells Nginx to maintain up to 32 idle connections in the pool. proxy_http_version 1.1; and clearing the Connection header are both required—without them, Nginx will still close connections immediately.
In support tickets I handled, enabling keepalive cut Apache's TIME_WAIT sockets by half and reduced average response time by 15–25 ms on busy sites. The difference is small per request but compounds under load.
2. Tune proxy buffering for large responses
Nginx buffers the entire Apache response in memory before sending it to the client. If a page is 2 MB and your buffer is 4 KB, Nginx writes the overflow to disk, which is slower.
Adjust buffer sizes to match your typical response profile:
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
For most PHP or WordPress responses, 8×16k (128 KB total) is enough. If you serve JSON APIs or large HTML pages, raise proxy_buffers to 16 or 32 buffers. Monitor disk I/O—if you see write spikes to /var/lib/nginx/proxy, your buffers are too small.
Disabling buffering (proxy_buffering off;) sounds appealing but makes Nginx wait for the client's network speed, tying up Apache workers longer. Leave buffering on unless you're streaming real-time data.
3. Cache static assets and dynamic pages selectively
Most guides show caching images and CSS in Nginx. That's table stakes. The real win is caching dynamic content—PHP-rendered pages, JSON endpoints—when they don't change per-user.
Set up a cache zone:
proxy_cache_path /var/cache/nginx/proxy levels=1:2 keys_zone=apache_cache:10m max_size=1g inactive=60m use_temp_path=off;
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://apache_backend;
proxy_cache apache_cache;
proxy_cache_valid 200 10m;
proxy_cache_use_stale error timeout updating;
proxy_cache_bypass $http_cache_control;
add_header X-Cache-Status $upstream_cache_status;
}
location ~* \.(jpg|jpeg|png|gif|css|js|woff2)$ {
proxy_pass http://apache_backend;
proxy_cache apache_cache;
proxy_cache_valid 200 1h;
expires 1h;
}
}
proxy_cache_use_stale updating is the secret. When Nginx is refreshing an expired cache entry, it still serves the stale version to other clients instead of queuing them behind Apache. That keeps response times flat during cache misses.
Exclude logged-in users or POST requests with proxy_cache_bypass $cookie_sessionid; or similar logic. Check your application's session cookie name first.
4. Strip or rewrite problematic headers
Apache often sends headers that confuse browsers, CDNs, or Nginx itself. Two common culprits: duplicate Content-Encoding headers and incorrect Connection: close.
Clean them up:
location / {
proxy_pass http://apache_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;
proxy_hide_header X-Powered-By;
proxy_hide_header Server;
}
proxy_hide_header removes headers from the upstream response. Hiding X-Powered-By and Server is basic security hygiene—no reason to advertise Apache's version string.
If Apache runs behind Cloudflare and Nginx, you'll also want to pass CF-Connecting-IP to Apache so logs show the real visitor IP, not 127.0.0.1. In Apache's LogFormat, use %{X-Real-IP}i or configure mod_remoteip to trust Nginx's forwarded headers.
5. Implement health checks and failover
A single proxy_pass line assumes Apache is always up. When it's not, Nginx returns 502 to every visitor until you restart things.
Run multiple Apache instances (or PHP-FPM pools) and configure upstream health checks:
upstream apache_backend {
server 127.0.0.1:8080 max_fails=3 fail_timeout=10s;
server 127.0.0.1:8081 backup;
keepalive 32;
}
The backup server is only used when the primary fails. You can also run Apache on different ports or even different machines. max_fails=3 fail_timeout=10s marks the upstream dead after three consecutive errors and won't retry for 10 seconds.
For true active health checks (Nginx sends periodic requests to test the backend), you need Nginx Plus or an external monitor like Keepalived. The open-source version only does passive failure detection.
6. Rate-limit and throttle at the proxy layer
Apache's mod_evasive and mod_qos work, but by the time a request hits Apache, you've already spent CPU cycles on the handshake and TLS. Handle rate limiting in Nginx before the proxy.
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
server {
listen 80;
server_name example.com;
location /wp-login.php {
limit_req zone=login_limit burst=2 nodelay;
proxy_pass http://apache_backend;
}
location / {
proxy_pass http://apache_backend;
}
}
This allows five login attempts per minute per IP, with a burst of two extra before enforcing the limit. nodelay means excess requests get a 503 immediately instead of queuing. Adjust the rate to match your application's expected usage.
You can also rate-limit API endpoints, search queries, or comment submissions the same way. The key is applying limits before Apache spins up a worker.
7. Offload compression to Nginx
Apache compresses responses with mod_deflate, but that means every dynamic request runs through gzip on the backend. Nginx is faster at compression and can serve precompressed static files.
Disable Apache's compression and move it to Nginx:
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://apache_backend;
proxy_set_header Accept-Encoding "";
}
}
Setting Accept-Encoding to empty tells Apache not to compress. Nginx compresses the response after receiving it from Apache. gzip_comp_level 5 is a good balance—higher levels save a few bytes but cost more CPU.
For static files, precompress them with gzip -k9 and use gzip_static on; so Nginx serves the .gz file directly without runtime compression.
8. Log and monitor upstream timing
When pages are slow, you need to know if the delay is in Apache, the network, or Nginx itself. Log upstream response times in your access log:
log_format proxy_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time';
access_log /var/log/nginx/proxy_access.log proxy_log;
$request_timeis the total request duration from Nginx's perspective.$upstream_connect_timeis how long it took to connect to Apache.$upstream_header_timeis time to first byte from Apache.$upstream_response_timeis the full backend response time.
If upstream_response_time is high but request_time is low, Apache is slow but Nginx is buffering it well. If both are high, Apache is the bottleneck. If upstream_connect_time is high, check Apache's MaxRequestWorkers or firewall rules.
Parse these logs with GoAccess, AWStats, or a custom script to identify slow endpoints.
So what if your setup is already fast?
Even if response times look acceptable, these eight moves improve reliability under load, reduce Apache's resource usage, and give you better visibility when things go wrong. The keepalive and caching changes alone cut server load enough that many sites defer a hardware upgrade.
Start with upstream keepalive and health checks—they're low-risk, high-impact changes. Then layer in caching, compression, and rate limiting as your traffic patterns demand. Monitor the logs after each change to confirm the improvement.
FAQ
Can I run Nginx and Apache on the same port?
No. Nginx listens on 80/443 externally, Apache listens on a different port (usually 8080 or 8081) on localhost. You bind only Nginx to the public IP.
Do I still need .htaccess if Nginx handles requests first?
Yes, for rules that affect Apache directly (PHP settings, directory protection). But rewrites, redirects, and caching are better moved to Nginx configuration.
How do I test if keepalive is working?
Run netstat -an | grep 8080 | grep ESTABLISHED and send a burst of requests with ab or curl. You should see persistent connections instead of a flood of TIME_WAIT states.
Should I cache POST requests?
No. POST requests modify data or carry session state, so caching them breaks functionality. Use proxy_cache_methods GET HEAD; to be explicit.
What size should I set for the cache path?
max_size=1g is a safe start. Monitor /var/cache/nginx/proxy disk usage with du -sh and raise it if your hit rate is low because entries are evicted too soon.
Start with keepalive and health checks
Most Nginx-Apache setups deliver pages but don't optimize the proxy itself. Enabling connection reuse, caching dynamic content, and monitoring upstream timing give you better performance and faster troubleshooting when load spikes or Apache hiccups. Implement these eight moves in stages, test with real traffic, and watch your backend relax.
