Skip to content

Fix: Nginx upstream timed out (110: Connection timed out) while reading response header

FixDevs · (Updated: )

Part of:  Docker, DevOps & Infrastructure

Quick Answer

How to fix Nginx upstream timed out error caused by slow backend responses, proxy timeout settings, PHP-FPM hangs, and upstream server configuration issues.

The Error

You check Nginx error logs and see:

upstream timed out (110: Connection timed out) while reading response header from upstream

Or variations:

upstream timed out (110: Connection timed out) while connecting to upstream
upstream timed out (110: Connection timed out) while reading upstream
upstream timed out (110: Operation timed out) while waiting for request

The browser shows a 502 Bad Gateway or 504 Gateway Timeout error. Nginx sent the request to the backend (upstream) server, but the backend did not respond within the configured timeout.

Why This Happens

Nginx acts as a reverse proxy, forwarding requests to a backend application (Node.js, Python, PHP-FPM, Java, etc.). When the backend takes too long to respond, Nginx gives up and returns an error to the client.

The phrase in the log message matters. while connecting to upstream means the TCP handshake never completed — the upstream is unreachable, refusing connections, or starved of workers. while reading response header means the connection succeeded but the backend has not produced any output yet. while reading upstream means the body started streaming but stalled mid-response. Each phrase points to a different layer of the stack, and the fix differs accordingly.

Timeouts also stack. A request from a browser usually traverses a CDN, a load balancer, Nginx, and then the application. Every hop has its own timeout, and the shortest one wins. If you raised Nginx’s proxy_read_timeout to 600s but your AWS ALB still uses the default 60s idle timeout, the client will see a 504 from the ALB long before Nginx gives up. Look at every layer when debugging.

Common causes:

  • Slow backend response. The application is processing a heavy query, generating a report, or waiting on a slow external API.
  • Backend is overloaded. Too many concurrent requests overwhelm the backend, causing response times to spike.
  • Default timeout is too low. Nginx’s default proxy timeouts are 60 seconds, which may not be enough for long-running operations.
  • Backend crashed or hung. The upstream process (PHP-FPM, Gunicorn, Node.js) is stuck or has crashed.
  • Connection pool exhaustion. The backend has no available workers to handle the request.
  • Network issues between Nginx and the backend. Firewall rules, DNS resolution failures, or network partitions.

Platform and Environment Differences

The same upstream timed out message behaves differently depending on where Nginx runs and what sits in front or behind it.

Protocol and directive prefix. Nginx applies timeout directives based on the upstream protocol, not just the location. proxy_read_timeout only affects proxy_pass upstreams. If you proxy to PHP-FPM through fastcgi_pass, the matching directive is fastcgi_read_timeout. If you proxy to a Python app through uwsgi_pass, it is uwsgi_read_timeout. gRPC upstreams use grpc_read_timeout. Setting proxy_read_timeout 600s in a FastCGI location does nothing — the directive is silently ignored.

FastCGI socket path per distribution. On Debian and Ubuntu the PHP-FPM socket lives at /var/run/php/php8.3-fpm.sock. On RHEL, CentOS Stream, Rocky Linux, and Alma it is /var/run/php-fpm/www.sock. On Alpine inside containers it is often a TCP port like 127.0.0.1:9000 rather than a UNIX socket. Copying an nginx config between distros without updating the socket path produces an instant connect() failed (2: No such file or directory) while connecting to upstream, which Nginx reports under the same upstream-error family.

Container topology. When Nginx and the backend run as separate containers on the same Docker network, you proxy to the service name (proxy_pass http://app:3000). DNS resolution and the bridge network add a few milliseconds and an extra failure mode (the service must be up before Nginx starts, or use resolver with valid= to refresh). When Nginx runs on the host and the backend in a container, you proxy to 127.0.0.1:<published-port>, which on Docker Desktop for Mac and Windows goes through a userland proxy that can stall under bursts. When both processes live on the same host, a UNIX socket is the fastest option but breaks the moment you scale horizontally.

Cloud load balancers add their own timeouts. AWS ALB has a default 60s idle timeout (configurable up to 4000s). CloudFront caps origin response at 30s by default (raisable to 60s, and only to 180s via support request). GCP HTTPS Load Balancer defaults to 30s. Cloudflare’s Free and Pro plans cut requests at 100s no matter what your origin does. Even if Nginx is patient, the layer in front returns 504 first and the client never sees Nginx’s response.

keepalive behavior changed across nginx versions. keepalive_requests defaulted to 100 in nginx 1.18 and earlier and to 1000 from nginx 1.19.10 onward. If you upgraded nginx and connection churn dropped, that is why. If you downgraded and saw new latency spikes, the smaller default is recycling sockets more aggressively.

Fix 1: Increase Proxy Timeouts

The most common fix. Increase the timeout values in your Nginx configuration:

location / {
    proxy_pass http://backend;
    proxy_connect_timeout 300s;
    proxy_send_timeout 300s;
    proxy_read_timeout 300s;
    send_timeout 300s;
}

What each timeout does:

DirectiveDefaultPurpose
proxy_connect_timeout60sTime to establish connection to upstream
proxy_send_timeout60sTime between successive writes to upstream
proxy_read_timeout60sTime between successive reads from upstream
send_timeout60sTime between successive writes to client

For long-running requests (file uploads, report generation), increase proxy_read_timeout since that is where the backend is processing:

location /api/reports {
    proxy_pass http://backend;
    proxy_read_timeout 600s;  # 10 minutes for report generation
}

After editing, test and reload:

nginx -t
sudo systemctl reload nginx

Pro Tip: Only increase timeouts for specific endpoints that need it, not globally. Setting a 10-minute timeout globally masks backend performance problems and allows slow requests to hold connections open unnecessarily.

Fix 2: Fix PHP-FPM Timeouts

If the upstream is PHP-FPM, you need to increase timeouts on both sides:

Nginx FastCGI timeouts:

location ~ \.php$ {
    fastcgi_pass unix:/var/run/php/php-fpm.sock;
    fastcgi_read_timeout 300s;
    fastcgi_send_timeout 300s;
    fastcgi_connect_timeout 300s;
    include fastcgi_params;
}

PHP-FPM pool settings (/etc/php/8.3/fpm/pool.d/www.conf):

request_terminate_timeout = 300

PHP execution time (php.ini):

max_execution_time = 300

Restart both after changes:

sudo systemctl restart php8.3-fpm
sudo systemctl reload nginx

Fix 3: Fix Backend Connection Issues

If the error says while connecting to upstream (not while reading), Nginx cannot reach the backend at all:

Check if the backend is running:

# For Gunicorn/Django
systemctl status gunicorn

# For Node.js with PM2
pm2 status

# For PHP-FPM
systemctl status php8.3-fpm

# For a generic process
ss -tlnp | grep <port>

Check the upstream address:

# Is this correct?
upstream backend {
    server 127.0.0.1:8000;
}

Verify the backend is listening on the expected port:

curl -v http://127.0.0.1:8000/

If the backend is not running, start it. If it is running but not responding, check its logs.

For general connection refused errors, see Fix: ERR_CONNECTION_REFUSED localhost.

Common Mistake: Configuring Nginx to proxy to localhost:8000 but the backend listens on 127.0.0.1:8000 (or vice versa on systems where localhost resolves to IPv6 ::1). Use 127.0.0.1 explicitly to avoid IPv4/IPv6 ambiguity.

Fix 4: Increase Backend Workers

The backend might have too few workers to handle incoming requests:

Gunicorn (Python):

gunicorn --workers 4 --timeout 120 myapp:app

Rule of thumb: workers = (2 * CPU cores) + 1.

uWSGI:

[uwsgi]
processes = 4
harakiri = 120

PHP-FPM:

pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35

Node.js with clustering:

const cluster = require("cluster");
const os = require("os");

if (cluster.isPrimary) {
    for (let i = 0; i < os.cpus().length; i++) {
        cluster.fork();
    }
}

Fix 5: Add Upstream Keepalive

Enable keepalive connections between Nginx and the backend to reduce connection overhead:

upstream backend {
    server 127.0.0.1:8000;
    keepalive 32;
}

location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

keepalive 32 maintains up to 32 idle connections to the upstream. This reduces the time spent establishing new TCP connections for each request.

Fix 6: Configure Load Balancing

If you have multiple backend servers, Nginx can distribute load and handle failures:

upstream backend {
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
    server 127.0.0.1:8003;

    # Fail timeout and max failures
    server 127.0.0.1:8001 max_fails=3 fail_timeout=30s;
}

If one server is slow, Nginx tries the next one:

location / {
    proxy_pass http://backend;
    proxy_next_upstream error timeout http_502 http_503;
    proxy_next_upstream_timeout 10s;
    proxy_next_upstream_tries 3;
}

proxy_next_upstream tells Nginx to try the next server when the current one returns an error or times out.

Fix 7: Add Request Buffering

Enable buffering to prevent the upstream from being held open while the client slowly receives the response:

location / {
    proxy_pass http://backend;
    proxy_buffering on;
    proxy_buffer_size 4k;
    proxy_buffers 8 16k;
    proxy_busy_buffers_size 32k;
}

With buffering, Nginx receives the entire response from the backend quickly, frees the upstream connection, and then sends the response to the slow client at its own pace.

For large responses (file downloads, exports):

proxy_max_temp_file_size 1024m;

Fix 8: Monitor and Debug

Check Nginx error logs:

tail -f /var/log/nginx/error.log

Check backend access times in Nginx logs:

Add upstream response time to the log format:

log_format upstream_time '$remote_addr - $remote_user [$time_local] '
                          '"$request" $status $body_bytes_sent '
                          '"$http_referer" "$http_user_agent" '
                          'upstream_response_time=$upstream_response_time '
                          'request_time=$request_time';

access_log /var/log/nginx/access.log upstream_time;

$upstream_response_time shows how long the backend took. If this is consistently close to proxy_read_timeout, your backend is too slow.

Check backend logs for the corresponding slow request:

# Gunicorn
journalctl -u gunicorn -f

# PHP-FPM
tail -f /var/log/php8.3-fpm.log

# Node.js PM2
pm2 logs

Still Not Working?

If the error persists after increasing timeouts:

Check for DNS resolution issues. If the upstream uses a hostname, DNS resolution might be slow:

upstream backend {
    server backend.local:8000;
}

Add a resolver:

resolver 127.0.0.1 valid=30s;

Check for SELinux blocking. On RHEL/CentOS, SELinux might block Nginx from connecting to the backend:

setsebool -P httpd_can_network_connect 1

Check for file descriptor limits. Under heavy load, Nginx might run out of file descriptors:

worker_rlimit_nofile 65535;
events {
    worker_connections 4096;
}

Check for upstream connection limits. Some backends limit concurrent connections. Check the backend’s configuration for connection pool sizes.

Check the load balancer in front of Nginx. AWS ALB defaults to a 60s idle timeout. Cloudflare’s Free and Pro plans cap responses at 100s. If a layer in front of Nginx times out first, raising Nginx timeouts does nothing. Confirm by reading the response headers — Server: awselb/2.0 or Server: cloudflare tells you which layer wrote the error page.

Check for slow client uploads filling buffers. When a client uploads a large body slowly, Nginx may hold the upstream connection open waiting for client_body_buffer_size to fill. Increase client_body_buffer_size or enable client_body_in_file_only on; so Nginx spools the body to disk before opening the upstream connection. The matching errors are usually about request-entity-too-large but also surface as upstream timeouts under bursty traffic.

Check for misrouted upstream blocks. If you use multiple upstream blocks and a proxy_pass references the wrong one, requests pile up on a server that is meant for a different workload. The symptom is upstream timed out only on a subset of routes. See Fix: Nginx upstream load balancing not working for diagnosing route-to-upstream mismatches.

Check for WebSocket connections held open without traffic. A WebSocket upgraded through Nginx counts against proxy_read_timeout because Nginx treats the lack of data as a stall. For long-lived sockets set proxy_read_timeout to something larger than the longest expected idle gap, or have the client emit periodic pings.

Check for OCSP stapling stalls during TLS handshakes to upstreams. If proxy_ssl_verify on; is set and the upstream’s certificate chain triggers an OCSP fetch, the first connection of the day can stall while Nginx waits on the OCSP responder. Add resolver and consider caching with proxy_ssl_session_reuse on;.

For 502 Bad Gateway errors (where the backend returns an error rather than timing out), see Fix: Nginx 502 Bad Gateway. For 504 errors visible to the client, see Fix: Nginx 504 Gateway Timeout.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles