Skip to content

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

Open
ycombinator wants to merge 4 commits into
mainfrom
feat/tcp-rejected-metric
Open

feat: add http_server.tcp_rejected_rate metric for connection-cap saturation#7499
ycombinator wants to merge 4 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

…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>
@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.

ycombinator and others added 3 commits July 29, 2026 00:22
…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>
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.

1 participant