Component(s)
processor/memorylimiter
Is your feature request related to a problem? Please describe.
The memorylimiterprocessor resolves limit_percentage / spike_limit_percentage against the container's cgroup memory limit once at startup (via iruntime.TotalMemory()) and converts them to fixed byte thresholds stored in memUsageChecker. These thresholds are never updated.
When Kubernetes In-Place Pod Vertical Scaling (GA in K8s 1.33, KEP-1287) resizes a container's memory limit, the kubelet updates memory.max in the cgroup filesystem. The memorylimiterprocessor continues using the stale thresholds from before the resize.
Impact
After an upscale (e.g., 896 MiB → 3000 MiB), the memorylimiter refuses data at the old thresholds (~537 MiB soft limit) while the container has gigabytes of unused memory. In my environment, this causes 30–40% of metric data points to be refused unnecessarily.
Reproduction
- Deploy an OTel collector with
memorylimiterprocessor configured with limit_percentage: 80 and spike_limit_percentage: 20 in a container with 896 MiB memory limit.
- Use a VPA or in-place resize mechanism to increase the container's memory limit to 3000 MiB.
- Observe the collector startup log:
memorylimiter: Using percentage memory limiter
total_memory_mib: 896, limit_percentage: 80, spike_limit_percentage: 20
memorylimiter: Memory limiter configured
limit_mib: 716, spike_limit_mib: 179
- Verify the cgroup was updated:
cat /sys/fs/cgroup/memory.max → 3145728000 (3000 MiB).
- The memorylimiter continues refusing data at 537 MiB (soft) / 716 MiB (hard) instead of the correct 1800 / 2400 MiB.
Describe the solution you'd like
Add a refresh_interval configuration field to memorylimiterprocessor that, when set to a non-zero duration, periodically re-reads the cgroup memory limit and recomputes the percentage-based thresholds.
Proposed config
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 20
refresh_interval: 10s # NEW: re-read cgroup memory limit every 10s
When refresh_interval is 0 (default), behavior is unchanged — thresholds are resolved once at startup.
Implementation sketch
The pattern already exists in the cgroupruntimeextension (extension/cgroupruntimeextension), which periodically re-reads the cgroup to update GOMEMLIMIT via automemlimit.FromCgroup(). The memorylimiter could follow the same approach:
- In
MemoryLimiter.Start(), if refresh_interval > 0, spawn a goroutine with a ticker.
- On each tick, call
GetMemoryFn() (which reads the cgroup via iruntime.TotalMemory()).
- If the value changed, recompute
memAllocLimit and memSpikeLimit and store them atomically.
- The existing
CheckMemLimits() loop reads these values on each check — no other changes needed.
The memUsageChecker fields (memAllocLimit, memSpikeLimit) would need to change from plain uint64 to atomic.Uint64 to support concurrent reads from the check loop and writes from the refresh loop.
Describe alternatives you've considered
- Restart the collector after resize — works but defeats the purpose of in-place resize (zero-downtime scaling).
- Use
cgroupruntimeextension with GOMEMLIMIT — we already do this, but GOMEMLIMIT only controls Go's GC target, not the memorylimiter's data-refusal thresholds. The two mechanisms are independent and currently work against each other after a resize.
- Custom processor wrapper — this is what we've implemented as a workaround, but it requires reimplementing the memorylimiter logic since the core types are in an
internal package.
Additional context
- The
cgroupruntimeextension already has refresh_interval for exactly this use case (tracking in-place resizes). The memorylimiter is the missing piece.
- Kubernetes 1.33 made InPlacePodVerticalScaling GA, so in-place resizing will become increasingly common.
- The
memorylimiterprocessor and cgroupruntimeextension both read the same cgroup memory.max file but have no integration point. Adding refresh_interval to the memorylimiter would close this gap without requiring cross-component coupling.
- Related: the
iruntime.TotalMemory() function (used by the memorylimiter) and memlimit.FromCgroup() (used by cgroupruntimeextension) both read the same cgroup filesystem path — there's no technical barrier to re-reading it.
Tip
React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it. Learn more here.
Component(s)
processor/memorylimiter
Is your feature request related to a problem? Please describe.
The
memorylimiterprocessorresolveslimit_percentage/spike_limit_percentageagainst the container's cgroup memory limit once at startup (viairuntime.TotalMemory()) and converts them to fixed byte thresholds stored inmemUsageChecker. These thresholds are never updated.When Kubernetes In-Place Pod Vertical Scaling (GA in K8s 1.33, KEP-1287) resizes a container's memory limit, the kubelet updates
memory.maxin the cgroup filesystem. Thememorylimiterprocessorcontinues using the stale thresholds from before the resize.Impact
After an upscale (e.g., 896 MiB → 3000 MiB), the memorylimiter refuses data at the old thresholds (~537 MiB soft limit) while the container has gigabytes of unused memory. In my environment, this causes 30–40% of metric data points to be refused unnecessarily.
Reproduction
memorylimiterprocessorconfigured withlimit_percentage: 80andspike_limit_percentage: 20in a container with 896 MiB memory limit.cat /sys/fs/cgroup/memory.max→3145728000(3000 MiB).Describe the solution you'd like
Add a
refresh_intervalconfiguration field tomemorylimiterprocessorthat, when set to a non-zero duration, periodically re-reads the cgroup memory limit and recomputes the percentage-based thresholds.Proposed config
When
refresh_intervalis 0 (default), behavior is unchanged — thresholds are resolved once at startup.Implementation sketch
The pattern already exists in the
cgroupruntimeextension(extension/cgroupruntimeextension), which periodically re-reads the cgroup to updateGOMEMLIMITviaautomemlimit.FromCgroup(). The memorylimiter could follow the same approach:MemoryLimiter.Start(), ifrefresh_interval > 0, spawn a goroutine with a ticker.GetMemoryFn()(which reads the cgroup viairuntime.TotalMemory()).memAllocLimitandmemSpikeLimitand store them atomically.CheckMemLimits()loop reads these values on each check — no other changes needed.The
memUsageCheckerfields (memAllocLimit,memSpikeLimit) would need to change from plainuint64toatomic.Uint64to support concurrent reads from the check loop and writes from the refresh loop.Describe alternatives you've considered
cgroupruntimeextensionwith GOMEMLIMIT — we already do this, butGOMEMLIMITonly controls Go's GC target, not the memorylimiter's data-refusal thresholds. The two mechanisms are independent and currently work against each other after a resize.internalpackage.Additional context
cgroupruntimeextensionalready hasrefresh_intervalfor exactly this use case (tracking in-place resizes). The memorylimiter is the missing piece.memorylimiterprocessorandcgroupruntimeextensionboth read the same cgroupmemory.maxfile but have no integration point. Addingrefresh_intervalto the memorylimiter would close this gap without requiring cross-component coupling.iruntime.TotalMemory()function (used by the memorylimiter) andmemlimit.FromCgroup()(used by cgroupruntimeextension) both read the same cgroup filesystem path — there's no technical barrier to re-reading it.Tip
React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding
+1orme too, to help us triage it. Learn more here.