Skip to content

interceptor: add HTTP keep-alive toggle#1571

Open
ikalnytskyi wants to merge 1 commit into
kedacore:mainfrom
ikalnytskyi:feat/disable-keep-alives
Open

interceptor: add HTTP keep-alive toggle#1571
ikalnytskyi wants to merge 1 commit into
kedacore:mainfrom
ikalnytskyi:feat/disable-keep-alives

Conversation

@ikalnytskyi
Copy link
Copy Markdown

@ikalnytskyi ikalnytskyi commented Apr 8, 2026

Persistent HTTP connections are not always helpful for all workloads. With synchronous/blocking backends, one interceptor can keep a connection open to a backend worker after a request finishes. Another interceptor may reuse that worker and wait until the first connection is closed, causing cross-replica blocking and higher latency.

Add KEDA_HTTP_DISABLE_KEEP_ALIVES so operators can disable connection reuse when this behavior appears.

Checklist

  • Commits are signed with Developer Certificate of Origin (DCO)
  • Changelog has been updated and is aligned with our changelog requirements
  • Any necessary documentation is added

@ikalnytskyi ikalnytskyi requested a review from a team as a code owner April 8, 2026 15:57
Copilot AI review requested due to automatic review settings April 8, 2026 15:57
@snyk-io
Copy link
Copy Markdown

snyk-io Bot commented Apr 8, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Persistent HTTP connections are not always helpful for all workloads.
With synchronous/blocking backends, one interceptor can keep a
connection open to a backend worker after a request finishes. Another
interceptor may reuse that worker and wait until the first connection is
closed, causing cross-replica blocking and higher latency.

Add `KEDA_HTTP_DISABLE_KEEP_ALIVES` so operators can disable connection
reuse when this behavior appears.

Signed-off-by: Ihor Kalnytskyi <ihor@kalnytskyi.com>
@ikalnytskyi ikalnytskyi force-pushed the feat/disable-keep-alives branch from 0f41d13 to 918fcd9 Compare April 8, 2026 15:58
@keda-automation keda-automation requested a review from a team April 8, 2026 15:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an interceptor configuration toggle to disable HTTP keep-alive connection reuse when it causes backend-side cross-replica blocking/latency.

Changes:

  • Adds KEDA_HTTP_DISABLE_KEEP_ALIVES to interceptor timeouts config.
  • Wires DisableKeepAlives into the interceptor’s upstream http.Transport.
  • Adds unit tests asserting Connection: close behavior when keep-alives are disabled.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
interceptor/proxy.go Plumbs DisableKeepAlives into the outbound http.Transport used to reach backends.
interceptor/proxy_test.go Adds tests + harness config to validate keep-alive disabling behavior.
interceptor/config/timeouts.go Introduces DisableKeepAlives env-configured field (KEDA_HTTP_DISABLE_KEEP_ALIVES).

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

Comment thread interceptor/proxy.go
Comment on lines 64 to 69
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialFunc,
ForceAttemptHTTP2: cfg.Timeouts.ForceHTTP2,
DisableKeepAlives: cfg.Timeouts.DisableKeepAlives,
MaxIdleConns: cfg.Timeouts.MaxIdleConns,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

DisableKeepAlives is intended to prevent backend connection reuse, but ForceAttemptHTTP2 is still enabled when cfg.Timeouts.ForceHTTP2 is true. With HTTP/2, a single TCP connection is typically reused/multiplexed across requests, so disabling HTTP/1.1 keep-alives may not achieve the desired "no reuse" behavior. Consider making DisableKeepAlives override/disable ForceAttemptHTTP2 (and/or documenting/enforcing that these options are mutually exclusive) so the toggle reliably prevents reuse.

Copilot uses AI. Check for mistakes.
@linkvt
Copy link
Copy Markdown
Member

linkvt commented Apr 9, 2026

Hi @ikalnytskyi , thanks for your PR!

I understand the solution you proposed but want to understand the problem as well as I'm not really sure about it right now.

Before that: There is currently one issue where existing backends continue to receive more traffic due to TCP connection to them being reused than new backend pods e.g. after scaling up.
When a new connection is made from the interceptor the k8s load balancing resolves an Pod IP, when existing connections are used this doesnt happen.
This should be worked around in most cases in #1473 as connections are then targeting the Pod IPs and not the Services ClusterIP.

Completely disabling connection reuse as done in this PR is quite a heavy solution and affects all InterceptorRoutes/HTTPScaledObjects with massive performance implications...
Is maybe decreasing the idle connection timeout a simpler solution?

Is this the problem that you have?
If not could you describe your exact use-case and problem better so that we're sure we're fixing the right thing? Which languages/frameworks are you using that cause the issue?

Thanks!

@linkvt linkvt self-requested a review April 9, 2026 08:05
@linkvt linkvt added the on hold label Apr 9, 2026
@ikalnytskyi
Copy link
Copy Markdown
Author

Hey @linkvt,

Sorry for the delay in my response.

want to understand the problem as well as I'm not really sure about it right now.

Let's assume we have two replicas (A and B) of keda-http-interceptor and a single replica of the upstream service.

  • When a user sends a request, keda-http-interceptor (A) opens a connection to my upstream server, serves the request, and puts the connection in the connection pool.
  • When a user sends a second request, it may hit either replica A or B.
  • If it hits A, everything works fine because it reuses the pooled connection.
  • If it hits B, it opens a new connection successfully (socket backlog allows it) and forwards the request, but the upstream server can't serve it because its synchronous worker is still occupied by A's pooled connection.

This happens when the upstream server respects the Connection header and doesn't close connections unless asked, and uses synchronous workers that process one request at a time. Common with Python WSGI servers that avoid threads due to GIL.

Completely disabling connection reuse as done in this PR is quite a heavy solution and affects all InterceptorRoutes/HTTPScaledObjects with massive performance implications...

I agree it should be configurable at the HTTPScaledObjects level since it depends on the upstream workload. Though by that logic, KEDA_HTTP_REQUEST_TIMEOUT and KEDA_HTTP_RESPONSE_HEADER_TIMEOUT should also be per-upstream since each upstream quite often has different SLAs.

I think saying the performance implications are massive is an exaggeration. It's really not that bad, especially without TLS. Nginx has used HTTP/1.0 for upstream proxying for years (i.e., no connection pooling by default), and many keep using it this way to date.

Is maybe decreasing the idle connection timeout a simpler solution?

That's the workaround that I use: decrease it to 1ms here, and then configure the connection pool to a couple of seconds in my upstream server to prevent races when one party hasn't yet sent FIN/RST. I just feel like this is a workaround, and I'd rather make sure the requests are forwarded to upstream with Connection: close to begin with.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants