java-topology/defects/nginx/patch/nginx-0004-upstream-keepalive-cache-linear-scan.md
russell@unturf.com 25c2bafdee undf: assign 694-720; stamp patches; ruby-0003/elixir-0002/r-source-0002/victoria-metrics-0002
New UNDF assignments (693→720):
  elixir-0002 → UNDF-2026-000000698 (typespec used_type_pairs O(T²))
  r-source-0002 → UNDF-2026-000000711 (.walkClassGraph match dedup O(S²))
  ruby-0003 → UNDF-2026-000000712 (RubyGems dependent_gems O(N²×D))
  victoria-metrics-0002 → UNDF-2026-000000717 (MetricName tag-filter O(T×I))

Total: 720 UNDF assigned
2026-03-29 22:28:31 -04:00

3.4 KiB
Raw Blame History

UNDF: UNDF-2026-000000707

nginx-0004 — ngx_http_upstream_keepalive: O(C) linear cache scan per upstream request

Ecosystem

nginx (C)

Severity

MEDIUM — hot path: executed on every upstream request that can reuse a keepalive connection; scales with keepalive directive value

Location

src/http/modules/ngx_http_upstream_keepalive_module.c

  • Function: ngx_http_upstream_keepalive_get_peer (~line 212)
  • Inner loop: for (q = ngx_queue_head(cache); q != ngx_queue_sentinel(cache); ...) (~line 229)

Description

When an upstream request needs a connection, ngx_http_upstream_keepalive_get_peer scans the entire keepalive cache queue to find a cached connection matching the upstream's sockaddr:

/* search cache for suitable connection */
cache = &kp->conf->cache;

for (q = ngx_queue_head(cache);
     q != ngx_queue_sentinel(cache);
     q = ngx_queue_next(q))                          // O(C) scan
{
    item = ngx_queue_data(q, ngx_http_upstream_keepalive_cache_t, queue);
    c = item->connection;

    if (ngx_memn2cmp((u_char *) &item->sockaddr, (u_char *) pc->sockaddr,
                     item->socklen, pc->socklen) == 0)
    {
        ngx_queue_remove(q);
        goto found;
    }
}

With keepalive N set to a large value (e.g., keepalive 1000 or keepalive 10000 for backends with many servers), this scan processes up to N entries on every upstream request. Under high throughput, this O(C) scan becomes a bottleneck:

  • 10,000 RPS × C=1000 keepalive cache entries = 10 million comparisons/second just for connection reuse lookups

Fix

Index the keepalive cache by sockaddr using a hash table keyed on the (family, addr, port) tuple, giving O(1) lookup per upstream request:

--- a/src/http/modules/ngx_http_upstream_keepalive_module.c
+++ b/src/http/modules/ngx_http_upstream_keepalive_module.c
@@ struct ngx_http_upstream_keepalive_srv_conf_s {
    ngx_queue_t              cache;      /* LRU queue of cached connections */
    ngx_queue_t              free;       /* free items */
+   ngx_hash_t               cache_hash; /* sockaddr → cached items list */

-  /* search cache for suitable connection */
-  for (q = ngx_queue_head(cache);
-       q != ngx_queue_sentinel(cache);
-       q = ngx_queue_next(q))
-  {
-      item = ngx_queue_data(q, ...);
-      if (ngx_memn2cmp(&item->sockaddr, pc->sockaddr, ...) == 0)
-          goto found;
-  }
+  /* O(1) hash lookup by sockaddr */
+  ngx_http_upstream_keepalive_cache_t *item =
+      ngx_hash_find(&kp->conf->cache_hash,
+                    ngx_crc32_long(pc->sockaddr->sa_data, pc->socklen),
+                    pc->sockaddr, pc->socklen);
+  if (item) goto found;

Complexity

Variant Cost per upstream connection attempt
Before O(C) — full scan of keepalive cache
After O(1) — hash map lookup
Speedup C× (keepalive pool size)

Notes

  • Default keepalive is typically 32-100 for simple setups; some deployments use 1000+ for microservice backends with many upstream servers
  • nginx's existing LRU queue serves eviction; the hash provides fast lookup
  • Multiple cached connections to the same server are valid; the hash can return a head-of-list and then remove from the LRU queue
  • ngx_queue_insert_head(&kp->conf->free, q) after successful lookup must also update the hash