Skip to content

Unbounded concurrent Monitoring API requests exhaust memory and OOM-kill the container #537 - #536

Open
ntmspavan wants to merge 3 commits into
prometheus-community:masterfrom
ntmspavan:fix/monitoring-max-concurrency-oom
Open

Unbounded concurrent Monitoring API requests exhaust memory and OOM-kill the container #537#536
ntmspavan wants to merge 3 commits into
prometheus-community:masterfrom
ntmspavan:fix/monitoring-max-concurrency-oom

Conversation

@ntmspavan

@ntmspavan ntmspavan commented Aug 7, 2026

Copy link
Copy Markdown

What this does

Each scrape currently fetches time series for every metric descriptor of
every configured project concurrently, with no limit (one goroutine +
HTTP request + JSON decode per descriptor, per project). When
google.projects.filter (or a long google.project-ids list) resolves to
many projects, this can spawn far more concurrent Monitoring API requests
than a memory-constrained pod can service at once, and lead to OOM kills —
particularly in containers, where GOMAXPROCS reports the host's vCPUs
rather than the container's cgroup CPU limit, so far more goroutines get
scheduled than the pod's actual quota, and CPU throttling stalls many of
them mid-decode with their buffers unfreed.

This adds monitoring.max-concurrency (default 0, unbounded — no
behavior change for existing users) which caps concurrent
TimeSeries.List requests via a single semaphore shared across all
resolved projects, so the limit holds regardless of how many projects are
matched.

Note: the semaphore intentionally only gates the per-descriptor
TimeSeries.List call, not the outer MetricDescriptors.List call — the
outer call synchronously waits on the goroutines it spawns, so sharing one
pool between both sites risks a nested-semaphore deadlock once enough
prefixes are in flight to exhaust the pool with outer callers.

Testing

  • make test / go test ./... — all existing tests pass
  • Added collectors/request_limiter_test.go, which spins up an
    httptest.Server and asserts concurrent TimeSeries.List requests never
    exceed the configured limit
  • go vet ./... clean

@SuperQ @kgeckhart could one of you take a look when you get a chance?

…currency

Each scrape fetches time series for every metric descriptor of every
configured project concurrently, with no limit. When google.projects.filter
(or a long google.project-ids list) resolves to many projects, this can
spawn far more concurrent Monitoring API requests/JSON decodes than a
memory-constrained pod can handle, leading to OOM kills.

Add monitoring.max-concurrency (default 0, unbounded) which caps concurrent
TimeSeries.List requests via a single semaphore shared across all projects,
so the limit holds regardless of how many projects are resolved.

Signed-off-by: Pavan Nalam <pavan.nalam3693@gmail.com>
Move the concurrency test into monitoring_collector_test.go (the
corresponding _test.go file for the changed code), extract semaphore
construction into a small newRequestLimiter helper with its own
table-driven test, and add direct unit tests for acquire/releaseRequestLimiter
covering the nil (unbounded) and blocking-when-full cases. Also assert
Config.MaxConcurrentRequests defaults correctly in TestNewConfigWithDefaults.

Signed-off-by: Pavan Nalam <pavan.nalam3693@gmail.com>
…ription

Signed-off-by: Pavan Nalam <pavan.nalam3693@gmail.com>
@ntmspavan ntmspavan changed the title Add monitoring.max-concurrency to bound per-scrape Monitoring API concurrency Add monitoring.max-concurrency to bound per-scrape Monitoring API concurrency #537 Aug 7, 2026
@ntmspavan ntmspavan changed the title Add monitoring.max-concurrency to bound per-scrape Monitoring API concurrency #537 Unbounded concurrent Monitoring API requests exhaust memory and OOM-kill the container #537 Aug 7, 2026
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