{"id":1130,"date":"2026-09-14T22:36:22","date_gmt":"2026-09-14T22:36:22","guid":{"rendered":"https:\/\/virtualserversvps.com\/blog\/?p=1130"},"modified":"2026-09-14T22:36:22","modified_gmt":"2026-09-14T22:36:22","slug":"nginx-upstream-keepalive-connection-pooling-vps","status":"publish","type":"post","link":"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/","title":{"rendered":"Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">By default, Nginx opens a brand-new TCP connection to every upstream (PHP-FPM, Node, Gunicorn, or a second Nginx) for every proxied request, then closes it. At 500 requests per second that is 500 handshakes and 500 teardowns per second \u2014 measurable as `TIME_WAIT` growth, rising p99 latency, and CPU spent in the kernel rather than in your application. Enabling upstream keepalive typically cuts end-to-end latency by 20\u201350% on connection-heavy workloads and drops ephemeral port pressure to near zero.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Measure the Problem Before You Fix It<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Two independent signals. First, connection churn in the upstream&#8217;s own logs; second, `TIME_WAIT` sockets on the Nginx host.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Load generator: 500 requests, 50 concurrent, against your Nginx\nwrk -t4 -c50 -d30s --latency http:\/\/127.0.0.1\/health\n\n# In a second shell, count churn while the test runs:\nwatch -n1 \"ss -tan | awk '{print \\$1}' | sort | uniq -c | sort -rn | head -5\"\n# Before tuning you will watch TIME_WAIT climb into the thousands.\n# Also check how many connections your upstream accepts:\nss -s | grep -i estab<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If `TIME_WAIT` climbs monotonically during the test and only drains afterwards, you are handshaking per request. If it stays flat, keepalive is already in play somewhere and the latency you are chasing is elsewhere.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Two-Directive Configuration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Upstream keepalive in Nginx needs a `keepalive` directive in the upstream block **and** a matching `proxy_set_header Connection` \u2014 the second one is what almost everyone forgets. The default `proxy_set_header Connection close;` in most example configs actively disables reuse.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>upstream app_pool {\n    server 127.0.0.1:9000;   # PHP-FPM, or 127.0.0.1:3000 for Node\/Gunicorn\n    server 127.0.0.1:9001;\n\n    keepalive 32;            # idle connections kept per worker to this upstream\n    keepalive_requests 1000; # reuse each connection this many times, then retire\n    keepalive_timeout 60s;   # idle timeout for pooled connections\n}\n\nserver {\n    listen 443 ssl http2;\n    server_name example.com;\n\n    location \/ {\n        proxy_pass http:\/\/app_pool;\n        proxy_http_version 1.1;          # REQUIRED - 1.0 cannot do keepalive\n        proxy_set_header Connection \"\";  # REQUIRED - clears the default 'close'\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\n        proxy_connect_timeout 2s;\n        proxy_send_timeout    30s;\n        proxy_read_timeout    60s;\n        proxy_next_upstream error timeout http_502 http_503;\n    }\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Sizing the keepalive Pool<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">`keepalive N` is the number of **idle** connections retained per worker process, not a total cap. The correct N depends on concurrency, not on request rate:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Concurrent upstream requests per worker<\/th><th>keepalive value<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td>&lt; 10<\/td><td>16<\/td><td>small sites, cron-driven traffic<\/td><\/tr><tr><td>10\u201350<\/td><td>32<\/td><td>typical single-app VPS<\/td><\/tr><tr><td>50\u2013150<\/td><td>64<\/td><td>busy API or WooCommerce checkout<\/td><\/tr><tr><td>&gt; 150<\/td><td>128<\/td><td>also raise worker_connections<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Each pooled connection costs one file descriptor and a kernel socket buffer \u2014 around 8 KB. Keeping 64 idle connections per worker on a 4-worker Nginx is 256 sockets, which is nothing. Do not set `keepalive 512` &#8220;just in case&#8221;; idle pooled connections to PHP-FPM still occupy a worker slot in the FPM pool, which is a real constraint on the other side.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Raise `worker_connections` in the `events` block to at least `keepalive_total + active_connections`, or Nginx will log `worker_connections are not enough` under load. For 4 workers with `keepalive 64` and a 1024 budget, `worker_connections 2048` is comfortable.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verify That Reuse Is Actually Happening<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Do not trust the config \u2014 count the sockets. After reloading, drive steady traffic and inspect the connection states on the upstream port:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nginx -t &amp;&amp; sudo systemctl reload nginx\nwrk -t2 -c20 -d20s http:\/\/127.0.0.1\/health &amp;\nsleep 3\n# Count established sockets to the upstream (9000 here). Expect a stable, small number.\nss -tan state established '( dport = :9000 or sport = :9000 )' | wc -l\n# And confirm TIME_WAIT is no longer growing:\nss -tan state time-wait | wc -l\nwait<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Before the change you would see the established count roughly equal to your concurrency and a `TIME_WAIT` count that keeps climbing. After it, the established count should settle at a number close to `keepalive \u00d7 workers` (or your concurrency, whichever is lower) and stay there, with `TIME_WAIT` flat. That flat line is the whole optimization.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Benchmark the Difference<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run the same load test twice \u2014 once with the `Connection` header defaulting to `close`, once with the keepalive block enabled \u2014 and compare the latency distribution, not the average. Connection setup shows up in the tail:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Before: comment out keepalive + proxy_http_version 1.1, then:\nwrk -t4 -c50 -d30s --latency http:\/\/example.com\/   | tee \/tmp\/before.txt\n# Apply keepalive config, reload, then:\nwrk -t4 -c50 -d30s --latency http:\/\/example.com\/   | tee \/tmp\/after.txt\ngrep -E 'Latency|50%|90%|99%|Requests\/sec' \/tmp\/before.txt \/tmp\/after.txt<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On a loopback PHP-FPM setup with a real framework boot per request, the typical result is 15\u201335% lower p99 and a 10\u201320% throughput increase, with the gain shrinking to near zero once your application&#8217;s own work dominates the request. If p99 does not move, your bottleneck is inside the application, and the connection layer was never the problem.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Failure Mode to Watch For<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Long-lived pooled connections to a backend that restarts will produce sporadic 502s, because Nginx may hand a request to a socket the backend has already closed. `keepalive_requests 1000` bounds how long a connection lives, and `proxy_next_upstream error timeout http_502 http_503` retries the request on a fresh connection. Set both. If your upstream is PHP-FPM, check `listen.allowed_clients` and that the FPM pool is not configured with `pm = ondemand` and a low `pm.max_children` \u2014 pooled connections count toward that limit even when idle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you are benchmarking this against a new host, <a href=\"https:\/\/virtualserversvps.com\/\">our VPS benchmark methodology<\/a> explains how to keep load-test results comparable across machines, and <a href=\"https:\/\/virtualserversvps.com\/#providers\">the provider comparison tables<\/a> show which plans have the CPU headroom to make upstream tuning visible in the first place. A single vCPU will saturate before connection reuse matters; two or more is where this pays off.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you are tuning a small VPS and want hardware that does not fight you, <a href=\"https:\/\/virtualserversvps.com\/#providers\">our VPS provider performance tables<\/a> break down CPU steal, NVMe IOPS, and RAM overcommit behaviour across the hosts we test on. Newer KVM nodes with dedicated vCPU pinning make the numbers in this article reproducible rather than aspirational. Two hosts we keep coming back to: <a href=\"https:\/\/interserver.net\/vps?id=1067805&amp;sid=virtualserversvps\" target=\"_blank\" rel=\"noreferrer noopener sponsored\">InterServer VPS<\/a> for flat-rate pricing with no RAM upcharge, and <a href=\"https:\/\/cloudways.com\/en\/?id=2010927&amp;data1=virtualserversvps\" target=\"_blank\" rel=\"noreferrer noopener sponsored\">Cloudways managed cloud<\/a> if you would rather not manage the kernel yourself. Compare the two against <a href=\"https:\/\/virtualserversvps.com\/\">the benchmark methodology we publish<\/a> before you commit.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>By default, Nginx opens a brand-new TCP connection to every upstream (PHP-FPM, Node, Gunicorn, or a second Nginx) for every proxied request, then closes it. At 500 requests per second&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":0,"footnotes":""},"categories":[3],"tags":[],"class_list":["post-1130","post","type-post","status-publish","format-standard","hentry","category-performance-optimization"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v26.1 (Yoast SEO v26.1) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server - Virtual Servers VPS Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server\" \/>\n<meta property=\"og:description\" content=\"Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server\" \/>\n<meta property=\"og:url\" content=\"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/\" \/>\n<meta property=\"og:site_name\" content=\"Virtual Servers VPS Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-14T22:36:22+00:00\" \/>\n<meta name=\"author\" content=\"Virtual-Servers-Vps-Editor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Virtual-Servers-Vps-Editor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"5 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/\",\"name\":\"Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-09-14T22:36:22+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"breadcrumb\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/virtualserversvps.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/\",\"name\":\"Virtual Servers VPS Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\",\"name\":\"Virtual-Servers-Vps-Editor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"caption\":\"Virtual-Servers-Vps-Editor\"},\"sameAs\":[\"https:\/\/virtualserversvps.com\/blog\"],\"url\":\"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server - Virtual Servers VPS Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/","og_locale":"en_US","og_type":"article","og_title":"Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server","og_description":"Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server","og_url":"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/","og_site_name":"Virtual Servers VPS Blog","article_published_time":"2026-09-14T22:36:22+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"5 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/","url":"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/","name":"Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-09-14T22:36:22+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"breadcrumb":{"@id":"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/virtualserversvps.com\/blog\/nginx-upstream-keepalive-connection-pooling-vps\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/virtualserversvps.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Nginx Upstream Keepalive: Cutting TCP Handshakes to Your App Server"}]},{"@type":"WebSite","@id":"https:\/\/virtualserversvps.com\/blog\/#website","url":"https:\/\/virtualserversvps.com\/blog\/","name":"Virtual Servers VPS Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0","name":"Virtual-Servers-Vps-Editor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","caption":"Virtual-Servers-Vps-Editor"},"sameAs":["https:\/\/virtualserversvps.com\/blog"],"url":"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/"}]}},"_links":{"self":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1130","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/comments?post=1130"}],"version-history":[{"count":1,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1130\/revisions"}],"predecessor-version":[{"id":1136,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1130\/revisions\/1136"}],"wp:attachment":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/media?parent=1130"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/categories?post=1130"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/tags?post=1130"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}