Skip to content

Prometheus Write backpressure retains per-target responses and can OOM subscribe mode #947

Description

@pood1e

Bug description

In subscribe mode, slow output transformation can retain up to target count × target buffer size complete gNMI SubscribeResponse protobufs. With many targets or large initial snapshots, memory grows until the process is OOM-killed.

The reproduced path is:

gRPC Recv
  -> Target.handleStreamSubscriptionRcv
  -> per-target subscribeResponses channel
  -> App.StartTargetsManager
  -> App.export
  -> prometheus_write.Write
  -> unbuffered msgChan
  -> limited output workers

Each target's receive channel defaults to 100 responses. App.export waits for the selected outputs, while prometheus_write.Write waits for an available worker through an unbuffered channel. When event processing is slower than incoming initial snapshots, every target independently fills its receive buffer with complete protobuf payloads.

After the output timeout, prometheus_write.Write returns and drops the ingress message. That drop is not included in the Remote Write failure metric because no HTTP request was attempted.

This is not an unreachable-object leak. It is multiplicative, reachable retention caused by the absence of a process-wide bounded input queue and explicit overload accounting.

Type

  • Performance defect
  • Reliability defect
  • Design defect

Reproduction

Tested in subscribe mode with main at c820a545e8c32325e15c5a8cd595965f9e57ae32:

  1. Start 220 active target entries backed by 20 simulators.
  2. Configure prometheus_write with 2 workers, 2 writers and a 10,000 time-series buffer.
  3. Point Remote Write at a local endpoint that immediately returns HTTP 204, excluding network and remote storage latency.
  4. Let all 220 streams reach READY and emit their initial snapshots.

Observed growth:

Sample Heap alloc Heap sys RSS Goroutines FDs
Initial 2.34 GB 2.85 GB 2.71 GB 2019 228
+44 seconds 3.17 GB 5.39 GB 5.23 GB 2018 228
+2 minutes 5.01 GB 5.98 GB 5.85 GB 2018 228

A heap profile reported 2.61 GB in use, with 2.40 GB (92.02%) retained below protobuf consumeBytes on the gRPC Recv -> Target.handleStreamSubscriptionRcv path. The goroutine profile showed target listeners blocked in promWriteOutput.Write while two workers transformed messages.

The Remote Write endpoint returned 204 and its send-failure metric stayed at zero. The retained objects are therefore upstream of HTTP Remote Write.

The same failure at larger scale becomes a repeated OOM/restart/failover loop. Adding memory only delays the failure because retained data grows with the backlog.

Expected behavior

  • prometheus_write has a process-wide bounded input queue shared by all targets.
  • Queue capacity, current depth and dropped ingress messages are observable.
  • Output overload cannot fill 100 response slots independently for every target.
  • Dropped ingress messages are distinct from HTTP Remote Write failures.
  • Memory remains bounded when input rate exceeds transformation throughput.

Actual behavior

  • prometheus_write.msgChan is unbuffered.
  • Target listeners wait up to the output HTTP timeout for a worker.
  • Each target has an independent 100-response receive buffer.
  • Ingress timeouts return silently unless debug logging is enabled.
  • Remote Write failure metrics remain zero for messages dropped before HTTP delivery.

Impact

  • Affected component: subscribe mode with CPU-heavy processors and prometheus_write.
  • Affected versions: reproduced on current main; the underlying behavior has existed across multiple releases.
  • User impact: collector OOM, repeated HA failover, missing telemetry and misleading delivery metrics.
  • Severity: high. A temporary output throughput reduction can take every collector replica out of service.

Introduction history

There is no single regression commit; the failure is the interaction of three changes:

While investigating this path, a separate collector-mode problem was found: #774 introduced goroutine-per-message output dispatch and later raised its pipeline capacity to 1,000,000. That path is not used by this reproduction and should be handled separately.

Related: #234.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions