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:
- Start 220 active target entries backed by 20 simulators.
- Configure
prometheus_write with 2 workers, 2 writers and a 10,000 time-series buffer.
- Point Remote Write at a local endpoint that immediately returns HTTP 204, excluding network and remote storage latency.
- 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.
Bug description
In
subscribemode, slow output transformation can retain up totarget count × target buffer sizecomplete gNMISubscribeResponseprotobufs. With many targets or large initial snapshots, memory grows until the process is OOM-killed.The reproduced path is:
Each target's receive channel defaults to 100 responses.
App.exportwaits for the selected outputs, whileprometheus_write.Writewaits 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.Writereturns 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
Reproduction
Tested in
subscribemode withmainatc820a545e8c32325e15c5a8cd595965f9e57ae32:prometheus_writewith 2 workers, 2 writers and a 10,000 time-series buffer.READYand emit their initial snapshots.Observed growth:
A heap profile reported 2.61 GB in use, with 2.40 GB (92.02%) retained below protobuf
consumeByteson thegRPC Recv -> Target.handleStreamSubscriptionRcvpath. The goroutine profile showed target listeners blocked inpromWriteOutput.Writewhile 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_writehas a process-wide bounded input queue shared by all targets.Actual behavior
prometheus_write.msgChanis unbuffered.Impact
subscribemode with CPU-heavy processors andprometheus_write.main; the underlying behavior has existed across multiple releases.Introduction history
There is no single regression commit; the failure is the interaction of three changes:
664f1a7e(2022-04-11) introduced the default per-target receive buffer of 100.c056a92f(2023-10-03) introduced the shared unbuffered Prometheus input channel, configurable workers and the enqueue timeout. It fixed I think Prometheus output have a memory leak that crashes the server it runs on. #234 for the tested rate but left ingress capacity and drops unobservable.41f58027(2025-09-22) introduced the currentApp.exportimplementation, which waits for output writes per target and therefore exposes the multiplicative target buffers when workers are saturated.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.