perf(nginx): enable HTTP/1.1 keepalive between nginx and gunicorn upstream#417
Open
mrrobot47 wants to merge 1 commit into
Open
perf(nginx): enable HTTP/1.1 keepalive between nginx and gunicorn upstream#417mrrobot47 wants to merge 1 commit into
mrrobot47 wants to merge 1 commit into
Conversation
…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).
There was a problem hiding this comment.
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 bothupstreamblocks to maintain an idle upstream connection pool. - Clear the proxied
Connectionheader inlocation @webserverviaproxy_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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue
Docker/nginx/template.confconfigures HTTP/1.1 between nginx and gunicorn but never enables connection reuse to the upstream:@webserverlocation (lines 82-92) setsproxy_http_version 1.1;but does NOT clear theConnectionheader, so each request still negotiates asConnection: close.upstream frappe-bench-frappeblock (lines 22-24) andupstream frappe-bench-socketio-serverblock (lines 25-27) have NOkeepalive 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 Nsetting is effectively dead code on this stack today. Operators who add--keep-aliveto 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:keepalive 64;to bothupstreamblocks so nginx maintains an idle connection pool to each upstream service.64is a conservative default that covers a Frappe worker pool of typical size without significantly increasing memory footprint.proxy_set_header Connection "";to the@webserverlocation, which is the canonical nginx idiom for upstream keepalive (clearing the inboundConnection: closeso HTTP/1.1 keep-alive can actually happen).The
/socket.iolocation is intentionally untouched — it correctly setsproxy_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.jsonis reasonable follow-up but not necessary for the fix;64is a safe default for the typical FM deployment shape.Test plan
fm create-style bench.nginx -T 2>&1 | grep -A2 'upstream frappe-bench-frappe'— confirmkeepalive 64;is present.apt-get install -y conntrack && conntrack -L 2>/dev/null | grep -c ':80 'before vs. after a burst of 100 sequentialcurlrequests 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./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 gainingkeepalive 64;Docker/nginx/template.conf:82-92—@webserverlocation gainingproxy_set_header Connection "";ngx_http_upstream_modulekeepalivedirective requires upstreamproxy_http_version 1.1and a clearedConnectionheader on the proxy block