What the 499 Error Means
Nginx 499 is not an HTTP standard โ it's a custom status code Nginx logs when the client closes the connection before the server finishes responding. You'll see lines like this in /var/log/nginx/access.log:
192.168.1.10 - - [27/Apr/2026:10:45:03 +0000] "GET /api/data HTTP/1.1" 499 0 "-" "Mozilla/5.0"
Notice the response body size is 0. Nginx never got to send anything โ the client hung up first.
Root Cause
Your backend was too slow. The client โ a browser, mobile app, CLI tool, or upstream load balancer โ ran out of patience and closed the TCP connection. Nginx was still waiting on PHP-FPM, Node.js, uWSGI, or a database query when the socket went dark.
The usual culprits:
- A database query that should take 50ms is taking 8 seconds because of a missing index
- The browser's built-in timeout (typically 30โ60 seconds) fires before the backend responds
- A load balancer upstream โ AWS ALB, HAProxy, Cloudflare โ has a shorter timeout and kills the connection first
- The user refreshes or navigates away mid-request
- A mobile client drops the connection on a flaky 4G network
A handful of 499s per hour is normal โ that's just users navigating away. Hundreds or thousands? Your backend has a real problem.
Step 1 โ Find Which Endpoints Are Slow
Don't touch config yet. First, find out which URLs are generating 499s and how long those requests actually ran:
# Count 499s by URL path
grep ' 499 ' /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
# Check request duration for those endpoints (requires $request_time in log format)
grep ' 499 ' /var/log/nginx/access.log | awk '{print $NF, $7}' | sort -rn | head -20
No $request_time in your logs? Add it to /etc/nginx/nginx.conf:
log_format main '$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';
Then reload: nginx -s reload
Step 2 โ Fix the Backend (This Is the Real Fix)
499s are a symptom. Raising timeouts only hides the problem. Profile and fix the backend first.
# For PHP-FPM: enable slow log to catch requests taking over 5 seconds
; In /etc/php/8.1/fpm/pool.d/www.conf
slowlog = /var/log/php-fpm-slow.log
request_slowlog_timeout = 5s
# For Node.js: look for blocking operations or unresolved promises
# For Python/Django/Flask: use django-silk or py-spy to pinpoint slow views
# Measure a specific endpoint directly
curl -o /dev/null -s -w "Connect: %{time_connect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" https://example.com/api/data
A response taking 12 seconds when it should take 200ms usually means a full-table scan or N+1 query. Fix that, and 80% of your 499s disappear without touching Nginx.
Step 3 โ Tune Nginx Proxy Timeouts
Some endpoints genuinely need extra time โ a bulk CSV export, a report that aggregates months of data. For those, raise the timeout on that specific location rather than globally:
# /etc/nginx/conf.d/your-site.conf
location /api/ {
proxy_pass http://backend;
proxy_connect_timeout 10s; # time to establish connection to upstream
proxy_send_timeout 60s; # time between writes to upstream
proxy_read_timeout 60s; # time to wait for upstream response
proxy_http_version 1.1;
proxy_set_header Connection "";
}
Override just the slow route โ don't raise every endpoint's budget:
location /api/export {
proxy_pass http://backend;
proxy_read_timeout 300s; # 5 minutes for this endpoint only
}
nginx -t && nginx -s reload
Step 4 โ Check Your Load Balancer's Timeout
When Nginx sits behind another layer, that layer may be cutting the connection before Nginx's timeout even fires.
AWS ALB defaults to a 60-second idle timeout. To raise it: EC2 โ Load Balancers โ select your ALB โ Attributes โ Idle timeout.
Cloudflare enforces a 100-second origin timeout on all plans. Endpoints that run longer need a different approach โ chunked responses, WebSockets, or offloading work to Cloudflare Workers.
HAProxy โ check timeout client and timeout server in your defaults block:
defaults
timeout connect 5s
timeout client 60s
timeout server 60s
Step 5 โ Handle 499s from User Navigation
Some 499s are unavoidable. A user hits Refresh at the wrong moment and Nginx logs a 499 โ but the upstream backend is still processing the request, consuming a worker thread with nobody waiting for it.
You can tell Nginx to let the upstream finish anyway:
# /etc/nginx/nginx.conf (http block)
proxy_ignore_client_abort on;
Use this with caution. It keeps the upstream connection alive and holds server resources even after the client is gone. Fine for short requests under 5 seconds; a bad idea for long-running exports where you might pile up zombie connections.
Step 6 โ Verify the Fix
# Watch for 499s dropping in real time
tail -f /var/log/nginx/access.log | grep ' 499 '
# Count 499s per minute over 5 minutes
for i in $(seq 5); do
echo -n "$(date +%H:%M): "
grep ' 499 ' /var/log/nginx/access.log | grep "$(date +%d/%b/%Y:%H:%M)" | wc -l
sleep 60
done
# Confirm slow endpoints improved (look for urt= times in log)
grep '/api/data' /var/log/nginx/access.log | tail -20
Prevention
- Per-route timeouts, not global ones. Fast endpoints should fail fast. Give only slow-by-design routes a longer budget โ otherwise a runaway query gets 300 seconds everywhere.
- Alert on 499 rate, not just count. When 499s exceed 1โ2% of total requests, something is genuinely broken โ not just users clicking Back. Wire that into PagerDuty, Grafana, or whatever you use.
- Move slow work off the HTTP thread. If a request consistently takes more than 5โ10 seconds, return a job ID immediately and let the client poll. Holding an HTTP connection open for 30 seconds under load will get you 499s.
- Pool your database connections. PgBouncer for PostgreSQL, or the connection pool in your ORM. Requests waiting for an available DB connection are the single most common cause of 499s in API backends.

