{"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-10-13T22:04:08","modified_gmt":"2026-10-13T22:04:08","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 on a VPS: Sizing Connection Pools With Measured Evidence"},"content":{"rendered":"<p class=\"wp-block-paragraph\">Without keepalive, every proxied request makes Nginx open a fresh TCP connection to your app server, run the handshake, serve, and tear it down. Under load this shows up as TIME_WAIT accumulation, ephemeral port pressure, and latency you cannot explain from app timings alone. Upstream keepalive fixes it \u2014 but the defaults and the copy-pasted <code>keepalive 32<\/code> line are frequently wrong for a small VPS. This article sizes the pool from measurement.<\/p><h2 class=\"wp-block-heading\">The two-zone configuration<\/h2><p class=\"wp-block-paragraph\">Keepalive needs a directive in the <code>upstream<\/code> block and a protocol version in <code>proxy_http_version<\/code>. Miss either and you get connection reuse silently disabled.<\/p><pre class=\"wp-block-code\"><code>upstream app_backend {\n    server 127.0.0.1:9000 max_fails=3 fail_timeout=10s;\n    keepalive 16;                 # idle keepalive conns cached per worker\n    keepalive_requests 1000;      # requests per conn before recycle\n    keepalive_timeout 60s;        # idle timeout of cached conns\n}\n\nserver {\n    location \/ {\n        proxy_pass http:\/\/app_backend;\n        proxy_http_version 1.1;   # REQUIRED for keepalive\n        proxy_set_header Connection \"\";\n        proxy_next_upstream error timeout http_502;\n    }\n}<\/code><\/pre><p class=\"wp-block-paragraph\"><code>proxy_set_header Connection \"\"<\/code> clears the <code>Connection: close<\/code> that Nginx otherwise forwards, which would defeat reuse.<\/p><h2 class=\"wp-block-heading\">Sizing <code>keepalive<\/code> from concurrency, not vibes<\/h2><p class=\"wp-block-paragraph\">The pool is per worker process. A reasonable starting point is the number of upstream connections a single worker needs concurrently, plus headroom:<\/p><figure class=\"wp-block-table\"><table><thead><tr><th>Metric<\/th><th>How to get it<\/th><th>Use<\/th><\/tr><\/thead><tbody><tr><td>Active upstream conns<\/td><td>ss -tn state established &#8216;( dport = :9000 )&#8217; | wc -l<\/td><td>Peak concurrency split across workers<\/td><\/tr><tr><td>Worker count<\/td><td>worker_processes auto<\/td><td>Divide concurrency by this<\/td><\/tr><tr><td>Requests\/sec<\/td><td>access log rate<\/td><td>Feeds keepalive_requests<\/td><\/tr><\/tbody><\/table><\/figure><pre class=\"wp-block-code\"><code># observe real upstream connection churn\nss -tn state established '( dport = :9000 )' | wc -l\nss -s | grep -i timewait\n\n# count new backend connections per second\nawk '{print $4}' \/var\/log\/nginx\/access.log | \\\n  cut -d: -f1 | uniq -c | sort -rn | head<\/code><\/pre><p class=\"wp-block-paragraph\">If TIME_WAIT on the upstream side drops to near zero after enabling keepalive, reuse is working. If it does not, verify <code>proxy_http_version 1.1<\/code> and the cleared <code>Connection<\/code> header before touching pool size. The same principle \u2014 verify the mechanism, then tune the number \u2014 applies across the <\/p><h2 class=\"wp-block-heading\">Memory and correctness on a small box<\/h2><ul class=\"wp-block-list\"><li>Each cached upstream connection holds a socket plus kernel buffers; on a 1 GB VPS keep pools modest (8\u201332, not 256).<\/li><li><code>keepalive_requests<\/code> too high lets a single connection live long enough to skew load balancing; 100\u20131000 is typical.<\/li><li><code>keepalive_timeout<\/code> longer than your upstream&#8217;s idle timeout causes reuse of connections the backend has already closed \u2014 a classic source of sporadic 502s.<\/li><li>Downstream and upstream keepalive are independent; tuning client keepalive does nothing for backend churn.<\/li><\/ul><p class=\"wp-block-paragraph\">Confirm the effect with request-level timing. Backend connect time should vanish from <code>upstream_connect_time<\/code>:<\/p><pre class=\"wp-block-code\"><code>log_format timed '$request_time $upstream_connect_time '\n                  '$upstream_header_time $upstream_response_time';\n# upstream_connect_time ~0.000 =&gt; a cached connection was reused<\/code><\/pre><h2 class=\"wp-block-heading\">Verifying reuse is real<\/h2><pre class=\"wp-block-code\"><code>nginx -T | grep -A6 'upstream app_backend'   # effective config\nss -tn '( dport = :9000 )' | tail -n +2 | wc -l\nss -s | grep -A2 TCP:                          # TIME_WAIT trend<\/code><\/pre><p class=\"wp-block-paragraph\">A healthy result: established upstream connections stay roughly flat during a load test while request throughput rises, and TIME_WAIT no longer grows without bound. Once keepalive is in place, the next bottleneck is usually the reverse-proxy layer itself \u2014 the keepalive, buffering and timeout directives in the <a href=\"https:\/\/virtualserversvps.com\/blog\/nginx-reverse-proxy-vps-setup-guide\/\">Nginx reverse proxy guide<\/a> cover the next stage.<\/p><p class=\"wp-block-paragraph\">Pair this with a database and PHP-FPM layer that can actually use the reduced connection overhead; the low-memory tuning approach keeps the app server from becoming the new ceiling.<\/p><h2 class=\"wp-block-heading\">Common ways keepalive silently stops working<\/h2><figure class=\"wp-block-table\"><table><thead><tr><th>Symptom<\/th><th>Cause<\/th><th>Fix<\/th><\/tr><\/thead><tbody><tr><td>upstream_connect_time never hits 0<\/td><td>proxy_http_version not set to 1.1<\/td><td>add proxy_http_version 1.1<\/td><\/tr><tr><td>Reuse works, then sporadic 502s<\/td><td>keepalive_timeout > backend idle timeout<\/td><td>lower it below the backend&#8217;s<\/td><\/tr><tr><td>Pool seems exhausted under load<\/td><td>keepalive too small for peak concurrency<\/td><td>raise it; observe with ss<\/td><\/tr><tr><td>No effect at all<\/td><td>Connection: close forwarded<\/td><td>proxy_set_header Connection &#8220;&#8221;<\/td><\/tr><\/tbody><\/table><\/figure><p class=\"wp-block-paragraph\">The second row is the one that wastes the most debugging time. If your application server closes idle connections after 30 seconds and Nginx caches them for 60, Nginx will hand a request to a socket the backend has already torn down. The upstream reports a reset, and Nginx returns 502. Set <code>keepalive_timeout<\/code> a few seconds below the backend&#8217;s idle timeout, not above it.<\/p><h2 class=\"wp-block-heading\">Load-testing the change safely<\/h2><p class=\"wp-block-paragraph\">Do not tune pool size against real traffic. Reproduce a representative load against the backend over loopback or a private network so you isolate the proxy behaviour:<\/p><pre class=\"wp-block-code\"><code># simple concurrency sweep with ab or wrk\nwrk -t4 -c100 -d30s http:\/\/127.0.0.1\/ &gt; baseline_keepalive_off.txt\n# enable keepalive, restart nginx, repeat\nwrk -t4 -c100 -d30s http:\/\/127.0.0.1\/ &gt; keepalive_on.txt\n# compare: requests\/sec, latency percentiles, TIME_WAIT count\ndiff &lt;(grep -E 'Requests\/sec|Latency' baseline_keepalive_off.txt) \\\n     &lt;(grep -E 'Requests\/sec|Latency' keepalive_on.txt)<\/code><\/pre><p class=\"wp-block-paragraph\">Watch three numbers across the sweep: requests\/sec should rise or stay flat, p99 latency should drop, and established upstream connections should stabilise instead of spiking with concurrency. If p99 does not improve, you are probably not CPU-bound at the proxy and the bottleneck lies downstream \u2014 reach for the app profiling workflow rather than adding more pool.<\/p><ul class=\"wp-block-list\"><li>Change one directive per test run; otherwise you cannot attribute the result.<\/li><li>Reset counters (<code>ss -s<\/code>) or restart between runs so TIME_WAIT from a prior run does not confuse the trend.<\/li><li>Keep the payload realistic \u2014 a static 2-byte response exercises connection handling, not the app.<\/li><\/ul><p class=\"wp-block-paragraph\">Once the pool is right, the pattern generalises: connection reuse is a systems property you verify with socket state, not a magic constant you paste. Apply the same discipline to downstream keepalive between your app server and database.<\/p>","protected":false},"excerpt":{"rendered":"<p>Without keepalive, every proxied request makes Nginx open a fresh TCP connection to your app server, run the handshake, serve, and tear it down. Under load this shows up as&#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":3,"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 on a VPS: Sizing Connection Pools With Measured Evidence - Virtual Servers VPS Blog<\/title>\n<meta name=\"description\" content=\"Without keepalive, every proxied request makes Nginx open a fresh TCP connection to your app server, run the handshake, serve, and tear it down. Under\" \/>\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 on a VPS: Sizing Connection Pools With Measured Evidence\" \/>\n<meta property=\"og:description\" content=\"Nginx Upstream Keepalive on a VPS: Sizing Connection Pools With Measured Evidence\" \/>\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 property=\"article:modified_time\" content=\"2026-10-13T22:04:08+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=\"4 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 on a VPS: Sizing Connection Pools With Measured Evidence - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-09-14T22:36:22+00:00\",\"dateModified\":\"2026-10-13T22:04:08+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"description\":\"Without keepalive, every proxied request makes Nginx open a fresh TCP connection to your app server, run the handshake, serve, and tear it down. Under\",\"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 on a VPS: Sizing Connection Pools With Measured Evidence\"}]},{\"@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 on a VPS: Sizing Connection Pools With Measured Evidence - Virtual Servers VPS Blog","description":"Without keepalive, every proxied request makes Nginx open a fresh TCP connection to your app server, run the handshake, serve, and tear it down. Under","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 on a VPS: Sizing Connection Pools With Measured Evidence","og_description":"Nginx Upstream Keepalive on a VPS: Sizing Connection Pools With Measured Evidence","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","article_modified_time":"2026-10-13T22:04:08+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"4 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 on a VPS: Sizing Connection Pools With Measured Evidence - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-09-14T22:36:22+00:00","dateModified":"2026-10-13T22:04:08+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"description":"Without keepalive, every proxied request makes Nginx open a fresh TCP connection to your app server, run the handshake, serve, and tear it down. Under","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 on a VPS: Sizing Connection Pools With Measured Evidence"}]},{"@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":3,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1130\/revisions"}],"predecessor-version":[{"id":1390,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1130\/revisions\/1390"}],"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}]}}