Skip to content

feat: add http_server.tcp_rejected_rate metric for connection-cap saturation - #7499

Merged
ycombinator merged 8 commits into
mainfrom
feat/tcp-rejected-metric
Jul 31, 2026
Merged

feat: add http_server.tcp_rejected_rate metric for connection-cap saturation#7499
ycombinator merged 8 commits into
mainfrom
feat/tcp-rejected-metric

Conversation

@ycombinator

@ycombinator ycombinator commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What is the problem this PR solves?

Fleet Server's existing autoscaling metric, http_server.tcp_active, is a utilization signal: EPA uses it to maintain headroom by targeting N active connections per pod. This works well at steady state but understates demand when connections are short-lived — specifically during large-scale agent enrollment, where each enrollment connection completes in under a second.

tcp_active is a point-in-time gauge: it reflects the instantaneous count of open HTTP connections at the moment EPA polls /stats. A burst of enrollment connections that arrive and clear between two EPA polls leaves no trace in the metric. The result is that pods plateau well below the theoretical requirement, agents receive HTTP 429s, and enter exponential backoff — turning a short enrollment phase into a multi-hour convergence tail.

How does this PR solve the problem?

Adds http_server.tcp_rejected_rate to /stats: a gauge, in rejections/sec, derived from an internal counter that increments whenever the chi Throttle middleware returns HTTP 429 due to the per-pod connection cap (max_connections). The rate is sampled every 10 s by a new RunConnRejectionRateSampler goroutine.

tcp_rejected_rate vs tcp_active

tcp_active tcp_rejected_rate
Signal type Utilization Saturation
When it fires Active connections approach the per-pod target Pod is already at its connection ceiling and returning 429s
Enrollment bursts Understates demand — burst clears before next EPA poll Catches the burst; rejections accumulate in the counter until sampled
Steady state Correct — long-held checkin connections register reliably Zero — pod is not at ceiling, no 429s
EPA behavior Scale to maintain headroom Scale the moment a pod is overwhelmed

Because EPA takes the maximum desired replica count across all configured metrics, wiring both allows proactive scaling (tcp_active: headroom at steady state) and reactive scaling (tcp_rejected_rate: act immediately under a burst). Neither metric suppresses the other.

The raw rejection count is kept as an unexported atomic.Uint64 — only the derived rate gauge is exposed in /stats, since that is the only value EPA consumes. The chi Throttle middleware behaviour is unchanged; the wrapper only observes the response status code.

How to test this PR locally

  1. Configure Fleet Server with max_connections: N and start it.
  2. Send more than N concurrent requests (e.g. with hey or wrk).
  3. curl localhost:5066/stats | python3 -m json.tool | grep tcp_rejected — confirm tcp_rejected_rate becomes non-zero after ~10 s.
  4. Let load drop — tcp_rejected_rate should return to 0 within the next sample interval.

Design Checklist

  • I have ensured my design is stateless and will work when multiple fleet-server instances are behind a load balancer.
  • I have or intend to scale test my changes, ensuring it will work reliably with 100K+ agents connected.
  • I have included fail safe mechanisms to limit the load on fleet-server: rate limiting, circuit breakers, caching, load shedding, etc.

Checklist

  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have made corresponding change to the default configuration files
  • I have added tests that prove my fix is effective or that my feature works
  • I have added an entry in ./changelog/fragments using the changelog tool

Related issues

@ycombinator
ycombinator requested a review from a team as a code owner July 29, 2026 07:17
@mergify

mergify Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

This pull request does not have a backport label. Could you fix it @ycombinator? 🙏
To fixup this pull request, you need to add the backport labels for the needed
branches, such as:

  • backport-./d./d is the label to automatically backport to the 8./d branch. /d is the digit
  • backport-active-all is the label that automatically backports to all active branches.
  • backport-active-8 is the label that automatically backports to all active minor branches for the 8 major.
  • backport-active-9 is the label that automatically backports to all active minor branches for the 9 major.

ebeahan
ebeahan previously approved these changes Jul 29, 2026
@mergify

mergify Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@ycombinator
ycombinator requested review from macdewee and swiatekm and removed request for blakerouse and lorienhu July 29, 2026 21:02
@swiatekm

Copy link
Copy Markdown
Member

The logic looks right to me, but calculating the rate of change this way within the application looks like an antipattern. Could we emit it as a total counter and calculate the delta downstream, either in a library or the monitoring solution?

@ycombinator

ycombinator commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

The logic looks right to me, but calculating the rate of change this way within the application looks like an antipattern. Could we emit it as a total counter and calculate the delta downstream, either in a library or the monitoring solution?

I had looked into this previously (for a similar trigger that we may/may not need: https://github.com/elastic/fleet-controller/issues/626). Unfortunately, Elastic Pod Autoscaler (EPA) doesn't support computing rates from a counter. We could implement this around fleet-controller, but that would require it to poll the /stats API on every Fleet Server pod, retain per-pod counter history, compute the rate, and expose the derived metric for EPA to consume — responsibilities it does not have today.

That would be a larger architectural change, so this PR keeps the rate calculation in Fleet Server for now.

[UPDATE] I've created https://github.com/elastic/elastic-pod-autoscaler/issues/745 so the EPA maintainers can discuss and decide if they want to support deriving rate metrics from counter metrics in the future.

ycombinator and others added 4 commits July 30, 2026 15:40
…uration

Adds tcp_rejected (counter) and tcp_rejected_rate (gauge, rejections/sec) to
Fleet Server's /stats endpoint. The rate is sampled every 10s from the count
of HTTP 429 rejections issued by the chi Throttle middleware (the per-pod
connection cap at max_connections), following the same pattern as the existing
checkin rejection rate sampler introduced in #7330.

tcp_rejected_rate is a saturation signal that complements tcp_active:
- tcp_active is a utilization signal — EPA uses it to maintain headroom by
  scaling before pods fill up. It understates demand when connections are
  short-lived (e.g. an enrollment burst arriving and clearing between samples).
- tcp_rejected_rate fires the moment a pod is at its connection ceiling and
  returning 429s, catching bursts that tcp_active misses entirely.

EPA takes the maximum desired replica count across all configured metrics, so
wiring both allows proactive scaling (tcp_active: maintain headroom at steady
state) and reactive scaling (tcp_rejected_rate: act immediately under a burst).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d in /stats

Only tcp_rejected_rate (the sampled gauge) is needed in /stats for EPA. The
raw accumulator doesn't need to be a first-class metric, so replace the
statsCounter (which registers with both libbeat and Prometheus registries) with
an unexported atomic.Uint64.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ectionRateSampler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…y noctx linter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ycombinator
ycombinator force-pushed the feat/tcp-rejected-metric branch from aae6116 to c85880b Compare July 30, 2026 22:40
Comment thread internal/pkg/api/router.go
Comment thread internal/pkg/api/router_test.go Outdated
Comment thread internal/pkg/api/metrics.go Outdated

@macdewee macdewee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We could also add test for tcp_rejected_rate should return to 0 within the next sample interval

@ycombinator

Copy link
Copy Markdown
Contributor Author

We could also add test for tcp_rejected_rate should return to 0 within the next sample interval

Added in 47aa51d.

@ycombinator
ycombinator enabled auto-merge (squash) July 31, 2026 13:19
@ycombinator ycombinator added backport-active-9 Automated backport with mergify to all the active 9.[0-9]+ branches backport-9.5 Automated backport to the 9.5 branch and removed backport-active-9 Automated backport with mergify to all the active 9.[0-9]+ branches backport-9.5 Automated backport to the 9.5 branch labels Jul 31, 2026
@mergify

mergify Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

This pull request does not have a backport label. Could you fix it @ycombinator? 🙏
To fixup this pull request, you need to add the backport labels for the needed
branches, such as:

  • backport-./d./d is the label to automatically backport to the 8./d branch. /d is the digit
  • backport-active-all is the label that automatically backports to all active branches.
  • backport-active-8 is the label that automatically backports to all active minor branches for the 8 major.
  • backport-active-9 is the label that automatically backports to all active minor branches for the 9 major.

@ycombinator
ycombinator merged commit dee344e into main Jul 31, 2026
14 checks passed
@ycombinator
ycombinator deleted the feat/tcp-rejected-metric branch July 31, 2026 14:26
@ycombinator ycombinator added the Team:Elastic-Agent-Control-Plane Label for the Agent Control Plane team label Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Team:Elastic-Agent-Control-Plane Label for the Agent Control Plane team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants