Skip to content

perf(nginx): enable HTTP/1.1 keepalive between nginx and gunicorn upstream#417

Open
mrrobot47 wants to merge 1 commit into
developfrom
perf/nginx-enable-upstream-keepalive
Open

perf(nginx): enable HTTP/1.1 keepalive between nginx and gunicorn upstream#417
mrrobot47 wants to merge 1 commit into
developfrom
perf/nginx-enable-upstream-keepalive

Conversation

@mrrobot47

Copy link
Copy Markdown
Member

Issue

Docker/nginx/template.conf configures HTTP/1.1 between nginx and gunicorn but never enables connection reuse to the upstream:

  • The @webserver location (lines 82-92) sets proxy_http_version 1.1; but does NOT clear the Connection header, so each request still negotiates as Connection: close.
  • The upstream frappe-bench-frappe block (lines 22-24) and upstream frappe-bench-socketio-server block (lines 25-27) have NO keepalive N; directive, so nginx maintains no idle pool to either upstream.

Net effect: every proxied request opens a brand-new TCP connection to gunicorn (or to the socketio server), incurring full TCP handshake overhead on the Docker bridge network. Connection reuse — which the HTTP/1.1 upgrade is specifically there to enable — is silently disabled.

Impact

Affects every FM-deployed site. A typical Frappe Desk page load fans out into dozens of /api/method/* calls within a few seconds; each one currently pays the per-request TCP-setup cost (typically 1-5 ms over the Docker bridge, more on busier hosts). At sustained high request rates the cumulative overhead becomes measurable on tail latency and reduces gunicorn's effective concurrency headroom (workers tied up on connection setup rather than request work).

A secondary impact: gunicorn's own --keep-alive N setting is effectively dead code on this stack today. Operators who add --keep-alive to gunicorn arguments expecting connection pooling get no benefit because the proxy in front never asks for a keep-alive in the first place. That's a confusing footgun.

Solution

Two minimal edits to Docker/nginx/template.conf:

  1. Add keepalive 64; to both upstream blocks so nginx maintains an idle connection pool to each upstream service. 64 is a conservative default that covers a Frappe worker pool of typical size without significantly increasing memory footprint.
  2. Add proxy_set_header Connection ""; to the @webserver location, which is the canonical nginx idiom for upstream keepalive (clearing the inbound Connection: close so HTTP/1.1 keep-alive can actually happen).

The /socket.io location is intentionally untouched — it correctly sets proxy_set_header Connection "upgrade"; for the WebSocket handshake, which is a different protocol concern.

Alternatives considered: making the pool size configurable via common_site_config.json is reasonable follow-up but not necessary for the fix; 64 is a safe default for the typical FM deployment shape.

Test plan

  1. Build the nginx image off this branch and bring up an fm create-style bench.
  2. Inside the nginx container: nginx -T 2>&1 | grep -A2 'upstream frappe-bench-frappe' — confirm keepalive 64; is present.
  3. From inside the nginx container: apt-get install -y conntrack && conntrack -L 2>/dev/null | grep -c ':80 ' before vs. after a burst of 100 sequential curl requests against any Frappe API endpoint. Before this PR the count grows roughly 1:1 with requests; after this PR it stays near a small constant.
  4. Confirm Frappe Desk still loads, /api/method/* requests still succeed, and the socketio (/socket.io) handoff is unaffected.

Rollback

Revert the merge commit. The change is purely additive within template.conf; no API/contract changes.

References

  • Docker/nginx/template.conf:22-27 — upstream blocks gaining keepalive 64;
  • Docker/nginx/template.conf:82-92@webserver location gaining proxy_set_header Connection "";
  • nginx docs: ngx_http_upstream_module keepalive directive requires upstream proxy_http_version 1.1 and a cleared Connection header on the proxy block

…tream

The @webserver location sets 'proxy_http_version 1.1;' but does not
clear the Connection header, and the frappe-bench-frappe upstream block
has no 'keepalive N;' directive. As a result nginx opens a fresh TCP
connection to gunicorn for every request, paying the full per-request
TCP-setup cost on the Docker bridge network and silently disabling
gunicorn's own --keep-alive setting.

Add 'keepalive 64;' to both upstream blocks and clear the Connection
header on the @webserver location so HTTP/1.1 connection reuse can
actually happen. /socket.io is intentionally untouched (its
Connection: upgrade header is required for the WebSocket handshake).
Copilot AI review requested due to automatic review settings June 23, 2026 12:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves reverse-proxy performance by enabling HTTP/1.1 upstream connection reuse between nginx and the Gunicorn (and socketio) upstream services in the Docker deployment template.

Changes:

  • Add keepalive 64; to both upstream blocks to maintain an idle upstream connection pool.
  • Clear the proxied Connection header in location @webserver via proxy_set_header Connection ""; so HTTP/1.1 keep-alive can actually be negotiated to the upstream.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants