Keepalive #

For every HTTP request sent by a client browser, the operating system in the background must first open a network connection at the TCP protocol level. Opening a new connection isn’t an instant process; it requires a handshake process back and forth between the client and server. When a modern web page loads dozens of static assets simultaneously, opening a new TCP connection for every asset is a very inefficient action.

This is where the Keepalive feature plays a crucial role. Keepalive allows the same TCP connection to be kept open after a request transaction finishes, so subsequent requests can be sent directly through that connection without needing a new handshake. In this article, we’ll discuss why the TCP connection creation process is so expensive, how to set client-side keepalive connections, correctly configure connection pooling to upstream backends, enable keepalive for PHP-FPM, and use Linux CLI utilities to verify the effectiveness of our keepalive connections.

Why Is Opening a New Connection So Expensive? #

There are three main factors that make repeatedly creating new TCP connections the main enemy of high-traffic server performance:

  1. TCP Three-Way Handshake: The TCP connection opening process requires three packet delivery steps: the client sends a SYN packet, the server answers with SYN-ACK, and the client sends back an ACK packet. This requires one full Round-Trip Time (RTT) before the first HTTP data can be sent.
  2. TLS Handshake Overhead: In the modern HTTPS era, after the TCP connection is established, the server and browser must perform a TLS encryption handshake. This process requires security certificate exchange and encryption key negotiation that takes 1 to 2 additional RTTs and consumes high CPU computing power for cryptographic processes.
  3. TIME_WAIT Socket Problems: When a TCP connection is closed, the socket isn’t immediately removed from the operating system. The socket enters a TIME_WAIT state for 60 to 120 seconds (depending on the Linux kernel configuration) to ensure no stray data packets are left on the network. If our server processes thousands of new requests per second without keepalive, the server runs out of local port number allocations (local port exhaustion) because tens of thousands of sockets are stuck in the TIME_WAIT state.

By using keepalive, we can cut this RTT and TLS negotiation latency by up to 90% for subsequent requests.


Client-Side Keepalive Connections (Browser to Nginx) #

Client keepalive controls how Nginx manages open TCP connections with user browsers after the response has been sent. We configure it using several directives inside the http or server block:

http {
    # 1. Idle connection retention timeout (Nginx default is 75s)
    # The recommended production sweet spot is 60 to 65 seconds
    keepalive_timeout 65s;

    # 2. Maximum number of requests allowed over one keepalive connection
    # After reaching this limit, Nginx forcibly closes the connection
    keepalive_requests 1000;

    # 3. Maximum total time one keepalive connection may be maintained
    keepalive_time 1h;
}

Understanding Client Keepalive Parameters #

  • keepalive_timeout 65s;: Nginx keeps the TCP connection open for 65 seconds after the last request activity finishes. If within 65 seconds the user does nothing (doesn’t click links or load new data), Nginx safely closes the connection. Setting this value too long (e.g., 5 minutes) can drain our worker_connections quota with empty connections from inactive visitors.
  • keepalive_requests 1000;: Modern browsers are very aggressive in loading web assets. The old Nginx default value (100) is often too small for Single Page Application (SPA) based web apps. Setting the limit to 1000 or more ensures browsers don’t need to do repeated TCP handshakes in the middle of a web page load.

Upstream-Side Keepalive Connections (Nginx to Backend) #

The performance tuning most often skipped by system administrators is configuring keepalive connections between Nginx and backend servers (like Node.js, Go, Python applications, etc.).

By default, Nginx acts as a stateless reverse proxy: for every request forwarded to the backend, Nginx opens a new TCP connection, takes the response from the backend, then immediately closes the connection. This default behavior incredibly wastes server resources on high traffic.

We must enable Connection Pooling inside the upstream block:

upstream app_backend {
    server 10.0.0.10:3000;
    server 10.0.0.11:3000;

    # Maintain a maximum of 32 idle connections in shared memory for each server
    keepalive 32;

    # Maximum time idle connections in the pool are maintained
    keepalive_timeout 60s;

    # Maximum requests per pool connection before replacing it with a new one
    keepalive_requests 2000;
}

Mandatory Configuration in the Server Block #

Declaring keepalive in the upstream block alone isn’t enough. We must insert two additional directives inside the location block so Nginx uses the HTTP/1.1 protocol that supports keepalive to the backend by default:

server {
    listen 80;
    server_name app.unisbadri.com;

    location / {
        proxy_pass http://app_backend;

        # 1. REQUIRED: Use HTTP/1.1 (proxy_pass defaults to HTTP/1.0 which doesn't support keepalive)
        proxy_http_version 1.1;

        # 2. REQUIRED: Empty the Connection header (removing the "close" instruction from client requests)
        proxy_set_header Connection "";

        proxy_set_header Host $host;
    }
}

[!IMPORTANT] Without declaring proxy_http_version 1.1; and proxy_set_header Connection "";, Nginx will still use the HTTP/1.0 protocol and send the Connection: close header to the backend for every request, making the keepalive parameter in our upstream block useless.

Here’s an illustration of the connection flow difference without keepalive vs with keepalive:

flowchart TD
    subgraph "Without Keepalive (HTTP/1.0)"
        c1["Client Browser"] -->|"1. TCP SYN / SSL"| n1["Nginx Proxy"]
        n1 -->|"2. TCP SYN (New Connection)"| b1["Backend API"]
        b1 -->|"3. Send Response"| n1
        n1 -->|"4. Close TCP Connection"| b1
        n1 -->|"5. Send Response & Close TCP"| c1
    end

    subgraph "With Keepalive (HTTP/1.1 Connection Pool)"
        c2["Client Browser"] -->|"1. One TCP Handshake / SSL"| n2["Nginx Proxy"]
        n2 -->|"2. Use an Idle Connection from the Pool"| b2["Backend API"]
        b2 -->|"3. Send Response"| n2

        c2 -->|"4. Send the Next Request"| n2
        n2 -->|"5. Reuse the Connection in the Pool"| b2
    end

    classDef default fill:#f9f9f9,stroke:#d1d5db,stroke-width:1px,color:#111827;
    classDef poolStyle fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#065f46;
    class n2,b2 poolStyle;

Determining the Right Keepalive Pool Capacity #

The keepalive parameter value in the upstream block determines the maximum number of idle connections maintained in shared memory for each backend server. This value isn’t a limit on the total active connection count, but only a reserve of ready-to-use connections during quiet server conditions.

We can determine the keepalive pool size using the following workload guide:

  • Low Traffic (< 100 req/s): keepalive 8;
  • Medium Traffic (100 - 1,000 req/s): keepalive 32;
  • High Traffic (> 1,000 req/s): keepalive 64; up to keepalive 128;

The Risk of Too-High Keepalive Values #

Don’t set the keepalive value excessively high (e.g., setting keepalive 1024 for a low-traffic server). This action forces our backend servers to leave thousands of connections open idly. Meanwhile, backend applications (like Node.js or PostgreSQL database servers) have far stricter maximum connection limits than Nginx. This can trigger socket exhaustion errors on the backend side (backend connection limits reached).


Keepalive for FastCGI / PHP-FPM #

If our backend is PHP-FPM connected to Nginx using the FastCGI protocol, we configure keepalive differently because FastCGI has its own connection protocol specification.

We must enable the fastcgi_keep_conn on; directive inside our server location block:

upstream php_backend {
    # Use a Unix socket for optimal local performance
    server unix:/run/php/php8.2-fpm.sock;

    # Create a keepalive pool for PHP-FPM
    keepalive 16;
}

server {
    listen 80;
    server_name myphpapp.com;

    location ~ \.php$ {
        fastcgi_pass php_backend;

        # REQUIRED: Enable keepalive specifically for the FastCGI protocol
        fastcgi_keep_conn on;

        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Unix Socket vs TCP Loopback Scenarios for PHP-FPM #

In local architectures where Nginx and PHP-FPM run on the same server machine, using a Unix Domain Socket (server unix:/run/php/php8.2-fpm.sock;) is a highly recommended choice because it avoids the local TCP/IP network stack overhead (TCP loopback). However, Unix sockets can’t be scaled if we want to put the PHP-FPM server on a separate machine (dedicated application servers).

If we must move the PHP-FPM backend to an external host over the network, we must switch to using a TCP Socket (e.g., server 10.0.0.25:9000;). In this multi-server TCP scenario, enabling fastcgi_keep_conn on; together with keepalive pooling becomes very important. Without keepalive, Nginx triggers the creation of millions of new cross-server TCP connections that can cause physical network queue congestion and local port allocation exhaustion.


Verifying Keepalive Is Working in Production #

We can prove whether Nginx’s keepalive connections to the backend are running optimally by monitoring socket activity on the server using built-in Linux terminal utilities like ss (socket statistics).

Run the following command on the production server terminal while serving traffic:

# Filter active TCP connections to our backend port (e.g., port 3000)
ss -tn state established | grep :3000

Analyzing Connection Patterns #

  • If Keepalive ISN’T Working: We’ll see a rapidly changing list of connections. The number of connections in the TIME_WAIT state will pile up heavily on the Nginx side:
    # Check the socket connection state count to the backend
    ss -atn | grep :3000 | awk '{print $1}' | sort | uniq -c
    
    A wrong output will show thousands of TIME-WAIT lines and very few ESTAB.
  • If Keepalive IS Working: We’ll see a stable and constant pool of connections with the ESTAB (Established) state. The TIME-WAIT state count toward the backend port will be very minimal because Nginx consistently reuses already-open connections.

TCP Keepalive Tuning at the Linux Kernel Level #

Besides configuring keepalive parameters in the Nginx configuration file, network socket performance and stability also heavily depend on how the Linux operating system kernel manages TCP keepalive in general. By default, Linux kernel settings for detecting whether an idle connection is still alive or dead (dead TCP connections) are designed for slow legacy computer networks.

For example, the Linux kernel default keeps dead TCP connections for 7200 seconds (2 hours) before sending a probe. On production web servers, letting dead sockets pile up for 2 hours is a dangerous waste of kernel memory.

We can optimize the kernel TCP keepalive parameters by adding the following lines to the /etc/sysctl.conf file on our host server:

# /etc/sysctl.conf
# Linux Kernel TCP Keepalive Optimization

# Waiting time (seconds) before sending the first probe on an idle connection
net.ipv4.tcp_keepalive_time = 300

# Pause time (seconds) between sending subsequent probe packets
net.ipv4.tcp_keepalive_intvl = 15

# Number of probe response failures before the connection is forcibly cut
net.ipv4.tcp_keepalive_probes = 5

After editing the file, run the following command to apply the changes to the kernel without needing to restart the server:

sudo sysctl -p

With this configuration, if a TCP keepalive connection is cut unilaterally (e.g., a client’s mobile phone suddenly loses internet signal), our Linux kernel detects it within 5 minutes (instead of 2 hours) and immediately frees the port number and socket memory resources automatically.


Protocol Evolution: HTTP/1.1 Keepalive vs HTTP/2 & HTTP/3 Multiplexing #

It’s important for us to understand how the role of Keepalive connections changes with the adoption of new web protocols:

  • HTTP/1.1 (Keepalive): Requests are sent sequentially. If there are 10 images to load, the browser must wait for image 1 to finish downloading before it can send the request for image 2 over the same TCP keepalive connection. This triggers the Head-of-Line Blocking (HoLB) problem at the HTTP level. To speed up, browsers usually open up to 6 parallel TCP keepalive connections to one server domain at once.
  • HTTP/2 (Multiplexing): Introduces the Multiplexing feature. Browsers only open a single TCP connection to our Nginx server. Through that one connection, browsers can send dozens of requests and receive dozens of responses concurrently without queuing for each other. This eliminates the need to open many parallel connections and radically reduces the handshake workload at Nginx.
  • HTTP/3 (QUIC over UDP): Goes further by replacing the TCP transport protocol with UDP using the QUIC protocol. In HTTP/3, there’s no more standard TCP handshake process. Instead, QUIC implements a single-step encrypted handshake system by default and uses a unique Connection ID, allowing users to switch networks (e.g., from office Wi-Fi to mobile 4G) without needing to disconnect and reconnect the network connection from scratch.

Although HTTP/2 and HTTP/3 reduce keepalive complexity on the client browser side, the keepalive connection pool configuration on the backend side (Nginx to upstream backend) remains 100% mandatory because that internal proxy communication mostly still relies on the standard HTTP/1.1 TCP protocol.


Summary and Best Practices #

  • Set keepalive_timeout to 65s: Keep client browser idle connections within a reasonable limit to balance server RAM memory with browser performance.
  • Must Use HTTP/1.1 to the Backend: Always declare proxy_http_version 1.1; and proxy_set_header Connection ""; when enabling keepalive in the upstream block.
  • Use fastcgi_keep_conn: Enable this feature specifically for PHP-FPM backends so FastCGI socket connections are continuously reused.
  • Adjust Pool Size: Set the keepalive value in the upstream block proportionally to the requests-per-second level to avoid socket exhaustion on the backend side.
  • Monitor TIME_WAIT Sockets: Use the ss command periodically to verify the health of internal network connections between the Nginx proxy and our backend.

← Previous: Caching   Next: Open File Cache →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact