Optimizing NGINX Keepalive Requests: Maximizing Web Server Performance Through Targeted Tuning

With nginx keepalive I reduce connection setup costs, minimize handshakes, and noticeably speed up response times. Carefully tuned timeouts, request limits per connection, and reused upstream sockets deliver measurable performance gains without requiring new hardware.

Key points

  • Timeouts Choose wisely: Keep idle time as short as necessary, but as long as useful.
  • Requests Limit per connection: Long-lasting sockets, no freezes.
  • Upstream Pools Enable: Persistent backend connections per worker.
  • Worker and adjust connections: Enough slots for idle and active clients.
  • Monitoring Establish: Monitor the connection rate, latency, and errors.

NGINX Keepalive: Impact and Costs

I deliberately keep TCP connections open because Handshakes are expensive and dominate in scenarios with many small requests. Persistent sockets not only save on RTTs, they also smooth out the CPU load, since cryptographic operations for TLS are triggered less frequently. However, each open connection consumes Resources, such as file descriptors and buffers, which I need to keep an eye on. The trick lies in finding the right balance: enough reuse for speed, enough capacity for new connections during peak loads. If you strike that balance, you’ll achieve consistently low TTFB values and a fast user experience.

HTTP/2 and HTTP/3: Multiplexing Meets Keepalive

With HTTP/2 and HTTP/3 The number of connections required per client decreases because multiple streams run over a single connection. Keepalive remains important, however: That one connection should remain reliably open; otherwise, the benefit of multiplexing is negated by frequent reconnections.

I pay attention to dedicated idle parameters for modern protocols and make sure the values match my client timeouts. For testing, I start with moderate settings and increase them once the load is stable, until the reconnection rate drops and latencies remain constant.

http {
    # HTTP/2: Idle timeout for unused but open streams
    http2_idle_timeout 60s;

    # HTTP/3/QUIC: similar logic for UDP-based connections
    http3_idle_timeout 60s;

    # TLS resumption reduces handshake overhead for reconnections
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
}

Multiplexing reduces the number of parallel TCP/QUIC connections required, but not the importance of the proper timeouts. If you use HTTP/2 or HTTP/3, you can often set client timeouts a bit more generously because many small resources are transmitted over the same channel. Important: Keep them measurable Time to First Byte, error rates, and open streams per connection.

Configuring Client Keepalive Correctly

For browser clients, I manage reuse via keepalive_timeout and keepalive_requests, so that sockets remain open long enough without blocking indefinitely. As a starting point, I use a timeout of 30–60 seconds and 100–300 requests per connection, then I adjust these values based on metrics. This article provides a detailed breakdown: Keepalive Timeout Guide, which explains the impact on latency and server resources. Shorter timeouts are suitable for a very large number of short calls, while longer timeframes are helpful for periodic API access. To get started, I set clear defaults and measure the effect on open connections and error patterns.

http {
    # Idle connections to the client
    keepalive_timeout 60s;
    # Upper limit on requests per TCP connection
    keepalive_requests 200;

    # Optional: Disable keep-alive for specific clients (legacy bugs)
    # keepalive_disable msie6;
}

Upstream Keepalive in the Reverse Proxy

Between NGINX and backend apps, I use persistent upstream sockets because establishing a connection to PHP-FPM, Node.js, or Python services also Latency costs. To do this, I activate an appropriate number of reusable connections per worker in the upstream pool. It’s important to use HTTP/1.1 for the downstream connection and an empty `Connection` header; otherwise, the client’s „close“ request breaks backend persistence. I base my configuration on the number of concurrent requests and set up the pool so that new connections are rarely required. This reduces the backend connect time, and the entire chain delivers faster Answers.

upstream backend {
    server 127.0.0.1:9000;
    keepalive 64; # Number of persistent upstream connections per worker
}

server {
    location / {
 proxy_pass http://backend;
        proxy_http_version 1.1;
 proxy_set_header Connection "";
 # TCP keepalive for upstream sockets at the OS level
 proxy_socket_keepalive on;
    }
}

Pool Sizing and Connection Budget

I calculate pools realistically: The number of persistent upstream connections is determined by worker_processes × keepalive per upstream. If you use 8 workers and a keepalive of 64, you’ll keep up to 512 sockets open per upstream—per instance. Behind a load balancer or when dealing with multiple upstream targets, this can quickly add up.

My target: enough open sockets so that the majority of requests without a new Connect is being met, but there is still room for peaks. I monitor the „new upstream connections per second“ metric and reduce it until further increases in the pool size no longer result in any significant improvement in latency.

I also take into account Fairness: Pools that are too large can put newly arriving clients at a disadvantage because worker slots are occupied by idle connections. A moderate limit combined with active monitoring is usually faster than setting maximum values based on guesswork.

Fine-Tuning: Timeouts and Request Limits

I combine timeouts and request limits in such a way that connections are effectively reused without causing Cross-country skier High values on both axes minimize the number of connections but increase the risk of stuck sockets in the event of network problems. Low values ensure fresh connections but require additional handshakes. I proceed in small steps, monitor errors, and make adjustments at regular intervals. The following table shows reasonable starting ranges for different usage patterns and provides a concise Orientation.

Scenario keepalive_timeout keepalive_requests Note
Many brief page views 10–30 seconds 100-300 Fast reuse, low idle overhead
Typical Website 60–120 seconds 200–400 A Solid Average for Assets and HTML
API with periodic calls 60–120 seconds 300–1000 Higher Reuse Rate for Clients
Internal Services / Gateways 30–90 seconds 500–1000+ Consistency is more important than a minimal number of connects

Worker Tuning and Connections

I put worker_processes Set it to "auto" or to the number of CPU cores, and make sure to allocate enough worker_connections because idle sockets occupy slots. Limits that are too low prevent new connections from being accepted, even though CPU capacity is still available. If you’re running large keepalive pools, you’ll need sufficient descriptors and event slots per worker. A good introduction to this topic can be found in „Scaling Worker Connections“, which explains the relationships between events, connections, and load. Carefully chosen values ensure that idle reuse and new connections can coexist.

worker_processes auto;

events {
    worker_connections 4096;
    # Optional: reuseport can improve distribution at the kernel level
    # multi_accept on;
}

http {
    keepalive_timeout 60s;
    keepalive_requests 200;

 upstream backend {
 server 127.0.0.1:9000;
 keepalive 64;
    }
}

Operating System and Socket Tuning

I'm checking system limits so that Keepalive can reach its full potential. Too few descriptors or tight socket queues result in artificial bottlenecks. In addition to `ulimit` and `worker_rlimit_nofile`, kernel limits are crucial.

# Sample sysctl values (adjust with caution and after testing)
fs.file-max = 1000000
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 16384
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 262144

I adjust these values to suit the environment: many short-lived connections benefit from a wider port range and shorter FIN/TIME_WAIT times. For upstream keepalive, I reduce Neuconnects, which reduces TIME_WAIT pressures. In addition, I take into account NAT-Devices between the proxy and the backend: Excessively strict idle timeouts on the network can cause connections to be terminated unpredictably. A moderate request limit per socket and TCP keepalives (proxy_socket_keepalive on;) prevent „stale“ connections.

Set the header and HTTP version correctly

I pay attention to HTTP/1.1 to the backend, because upstream keep-alive only works that way. I also remove active connection control via headers so that NGINX manages persistence on its own. On the client side, I run Keep-Alive in accordance with the standard and limit the connection lifetime using timeouts and request limits. In addition, I check backend idle timeouts and set them slightly higher than NGINX’s to avoid reset errors. Clean headers ensure the Reuse without unintended closures.

# Example: Proxy location with correct headers
location /api/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

Distinction: HTTP Keep-Alive vs. TCP Keep-Alive

I make a strict distinction between HTTP Keep-Alive (multiple HTTP requests per connection) and TCP-Keepalive (OS-level probes to detect dead endpoints). I control HTTP Keep-Alive with keepalive_timeout and keepalive_requests, while TCP keepalives, depending on the stack, can be set to proxy_socket_keepalive on; and system parameters. For backends running on unstable networks, I enable TCP keepalives to clear up stuck sockets more quickly.

Long-running processes and special cases: WebSockets, SSE, gRPC

WebSockets and server-sent events are Cross-country skier, which keep a connection open for a long time—classic reuse plays a minor role here. I make sure to use the appropriate proxy_read_timeout and protect me with send_timeout against Slowloris-Effects. For gRPC (HTTP/2-based), multiplexing considerations apply; I configure idle timeouts so that streams aren't unnecessarily terminated.

location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 300s;
    send_timeout 30s;
}

Monitoring and metrics

I measure success using metrics such as the rate of new upstream connections, upstream_connect_time and the proportion of open connections per worker. Declining connect rates while request volumes remain constant or increase indicate successful reuse. Notable timeouts or connection resets signal inconsistent timeouts between NGINX and the backend. In addition, I monitor memory, file descriptors, and event queues under load. Regular monitoring allows you to identify trends early and prevent costly Failures.

Logging Improvements for Reuse Transparency

To gain more insight, I'm adding connection details to the access log. This allows me to see how often a TCP connection is reused and how connection times are changing over time.

log_format keepalive_fmt
  '$remote_addr $host "$request" $status $body_bytes_sent '
  '$request_time $upstream_connect_time '
  'conn:$connection reqs:$connection_requests';

access_log /var/log/nginx/access_keepalive.log keepalive_fmt;

I am tracking the median and P95/P99 values of upstream_connect_time as well as the distribution of $connection_requests. Rising Reuse counts with stable latency indicate that the pools and timeouts are set appropriately.

Typical stumbling blocks and solutions

Pools that are too large fill up connection slots while new clients wait, so I keep the sizes moderate and monitor them. Different idle timeouts between the proxy and the backend cause resets, so I set the backend's timeout to be just slightly higher than NGINX. A forgotten „Connection: close“ in the proxy header breaks persistence, so I consistently clear the header. TLS negotiation can put a strain on the CPU when there are many new connections, which I mitigate by increasing the reuse rate. In the event of sporadic network errors, a moderate request limit per socket helps ensure that old Sessions We cannot live forever.

Real-world configurations

For high-traffic websites, I choose a short timeout and a medium-high request limit to ensure assets load efficiently. For APIs with recurring calls, I raise the limit to further reduce TCP and TLS handshakes. I size upstream pools based on expected concurrency and test them with realistic traffic. Every environment behaves differently, so I check latency and error patterns after making changes. Two examples illustrate this: starting values, which I then refine using metrics.

# Scenario 1: High-Traffic Website
http {
    keepalive_timeout 30s;
    keepalive_requests 300;

 upstream app {
 server 127.0.0.1:8080;
        keepalive 32;
    }

 server {
 listen 443 ssl http2;
 Monitoring # HTTP/2 Idle Time
 http2_idle_timeout 45s;
    }
}
# Scenario 2: API with Periodic Calls
http {
    keepalive_timeout 75s;
    keepalive_requests 1000;

    upstream api_backend {
 server 127.0.0.1:9001;
 keepalive 64;
    }

    server {
 listen 443 ssl http2;
 # Slightly longer idle window for recurring calls
 http2_idle_timeout 75s;
    }
}

Checklist for Iterative Optimization

I'll start by analyzing the current situation: traffic patterns, response times, and error rates set the pace. Next, I'll set the client timeout and request limit to reasonable default values and enable upstream pools. I'll set the backend idle timeouts slightly higher than in NGINX to prevent any unexpected resets occur. I then monitor connection rates, connect time, and open sockets per worker. If you want to delve deeper into the reuse rate, you'll find suggestions on Connection Reuse and reasonable upper limits.

Additional Diagnosis: Mismatches and Time Behavior

When connections seem to drop „for no reason,“ I look for Mismatches in the chain: Client Idle vs. NGINX Timeout vs. Backend Idle and intermediate NAT/gateways. I increase the backend timeout slightly beyond the NGINX value, check for reset codes in the error log, and observe whether upstream_connect_time shows spikes. Often, a small buffer (e.g., +10–20%) for the backend timeout is enough to eliminate resets.

I also note that „lingering close“-Phases: When closing connections, NGINX briefly allows incoming data to flow through, which ties up worker resources. A very large number of simultaneous closures can tie up events. In such cases, I adjust the closure time windows and keep the total number of open connections in check by setting appropriate keepalive values.”.

Summary: Keepalive as a Performance Lever

I use Keepalive strategically because it reduces connection setup costs, lowers latency, and reduces the load on the CPU. The combination of an appropriate timeout, a reasonable request limit, and suitable upstream pools results in noticeable Speed. Without monitoring, potential goes untapped, which is why I continuously review key metrics and adjust values step by step. If you need additional resources, pay attention to the number of workers, connection slots, and proper header handling. Professional setups, such as those at webhoster.de, they make full use of these controls and provide fast, reliable services.

Current articles