Skip to content

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

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

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

Conversation

@ntmspavan

@ntmspavan ntmspavan commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #537. Replaces the earlier monitoring.max-concurrency flag (from ba53328/6e46846/4e1593c on this branch) with two fixes that require no configuration, since a manual flag defaulting to "unbounded unless you know to set it" doesn't actually solve "respect the pod's resource limits by default."

Problem

reportMonitoringMetrics spawns one goroutine per metric descriptor per project, with no cap on fan-out. When google.projects.filter (or a long google.project-ids list) resolves to many projects, or a project has many metric descriptors, the number of concurrently in-flight TimeSeries.List requests -- each holding a fully decoded JSON response in memory -- is unbounded. In a memory-constrained pod this can spike the heap past the container's memory limit and get it OOM-killed.

Fix

  1. Bound the actual fan-out. TimeSeries.List requests are now capped process-wide at a fixed internal limit (maxConcurrentTimeSeriesRequests = 20 in collectors/monitoring_collector.go), shared across every project's collector. This directly caps the peak live memory a scrape can hold, regardless of how many projects or descriptors are involved. This is a hardcoded internal constant, not a flag -- it's a bug fix to unbounded behavior, not a new tunable.

  2. Make the GC aware of the container's actual limit. At startup, stackdriver_exporter now reads the container's cgroup memory limit and sets Go's GOMEMLIMIT to 90% of it via automemlimit (https://github.com/KimMachineGun/automemlimit). This is a defense-in-depth backstop: the GC reclaims more aggressively as usage approaches the real limit. It activates automatically whenever resources.limits.memory is set on the container; it's a no-op otherwise (unlimited pod, bare VM with no cgroup cap, non-Linux). Can be overridden with the GOMEMLIMIT/AUTOMEMLIMIT env vars.

Neither of these reads or depends on resources.limits.cpu/GOMAXPROCS -- that's a separate, unaddressed concern (tracked as a possible future automaxprocs addition, not part of this PR).

Testing

  • go build ./..., go vet ./..., gofmt -l . all clean.
  • go test -race ./... passes.
  • Added TestTimeSeriesRequestLimiterBoundsConcurrency, which fails if concurrency ever exceeds the limit or never reaches it (so it can't pass without exercising real contention).

…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
The manual monitoring.max-concurrency flag required an operator to guess
a concurrency number correlated only loosely with actual memory usage,
and did nothing by default (unbounded) unless explicitly set. Replace it
with two fixes that require no configuration:

- Cap concurrent Monitoring API TimeSeries.List requests process-wide at
  a fixed internal limit (maxConcurrentTimeSeriesRequests), so a scrape
  can never hold more than that many decoded API responses in memory at
  once, regardless of how many projects or metric descriptors it fans
  out across.
- Read the container's cgroup memory limit at startup and set Go's
  GOMEMLIMIT to 90% of it via automemlimit, so the garbage collector
  reclaims more aggressively as usage approaches the container's actual
  resource limit.

Signed-off-by: Pavan Nalam <pavan.nalam3693@gmail.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.

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

1 participant