Skip to content

CPU busy-loop in ClientReportRecorder.recordLostEnvelope → SentryEnvelopeItem.getLogs() when envelopes are discarded under rate limiting (8.49.0) #5830

Description

Integration

sentry-spring-boot (Spring Boot 4)

Java Version

25 (Eclipse Adoptium 25.0.3)

Version

8.49.0

Steps to Reproduce

Conditions under which we hit this in production (2 replicas, both affected):

  1. logs.enabled=true (Sentry Logs feature on), traces-sample-rate > 0.
  2. The org/project hits its span/transaction quota — Sentry starts rate-limiting the transaction/span/monitor categories (429 / X-Sentry-Rate-Limits). Log quota was NOT exhausted.
  3. As soon as envelopes start being discarded (rate limiting, and subsequently queue overflow), the SDK enters a hard CPU busy-loop: the SentryAsyncConnection and SentryExecutorService threads each pin one CPU core at 100%, indefinitely.

Restarting the JVM does not help — a freshly booted instance re-enters the loop within seconds, as long as the rate limiting is active. Event delivery from the affected instance stops entirely. Setting logs.enabled=false prevents the loop (our current workaround, together with traces-sample-rate=0 to stop feeding the rate-limited categories).

Expected Result

Discarding rate-limited envelopes and recording client reports should be O(1)-ish and quiet — no sustained CPU burn, and no impact on delivery of non-rate-limited categories (errors).

Actual Result

Async-profiler (via Pyroscope) shows the loop clearly. CPU is dominated by exception construction — java.lang.Throwable::fill_in_stack_trace is the single largest self-time frame — reached through this path:

java.util.concurrent.ThreadPoolExecutor$Worker.run
  io.sentry.clientreport.ClientReportRecorder.recordLostEnvelope
    io.sentry.clientreport.ClientReportRecorder.recordLostEnvelopeItem
      io.sentry.SentryEnvelopeItem.getLogs                <- deserializes the log payload to count items
        io.sentry.JsonSerializer.deserialize
          io.sentry.SentryLogEvents$Deserializer.deserialize
            io.sentry.SentryLogEvent$Deserializer.deserialize
              io.sentry.JsonObjectReader.beginObject / nextListOrNull
                io.sentry.vendor.gson.stream.JsonReader.beginObject
                  java.lang.IllegalStateException.<init>  <- thrown at extreme rate
                    java.lang.Throwable.fillInStackTrace  <- dominates the CPU profile

The JsonObjectReader recovery machinery (beginRecovery/recoverValue/recoverAfterValueFailure/skipValue) also shows up prominently, suggesting the deserializer keeps throwing and "recovering" per token over the same payload, per discarded envelope, in a tight loop.

Relative weights from a 30-minute profile window (two threads pinned the whole time):

frame share of profile
SentryLogEvents$Deserializer.deserialize (total) ~1.9x of one core-window
ClientReportRecorder.recordLostEnvelope (total) ~0.97x
Throwable.fillInStackTrace (total) ~1.4x
IllegalStateException.<init> (total) ~0.73x

Client reports from the affected day also show the feedback loop (values are per ~24h, one service): transaction/client_discard: 711850, span/client_discard: 773325, log_byte/client_discard: 8054 — far beyond what the service organically produces, consistent with the discard path re-processing items continuously.

Happy to provide the full flamegraph JSON privately if useful.

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status
    Waiting for: Product Owner

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions