Skip to content

App Hang Tracking V2 reports false "App Hang Fully Blocked" events when a CarPlay scene keeps the app alive while the phone scene is backgrounded #8317

Closed as not planned

Description

@sbs44

Platform

iOS

Environment

Production

Installed

CocoaPods

Other Error Monitoring Solution

No

Other Error Monitoring Solution Name

No response

Version

9.18.0

Xcode Version

EAS Build toolchain (Expo SDK 56 default)

Did it work on previous versions?

Unknown... we see the same false positives across our releases going back ~6 weeks (all on cocoa 9.x with App Hang Tracking V2 default-enabled)

Steps to Reproduce

  1. iOS app with the CarPlay entitlement that presents a CPWindow scene (turn-by-turn navigation app), plus background location.
  2. Start navigation with CarPlay connected.
  3. Lock the phone (or background the phone app) while the CarPlay scene stays active. The process keeps running and rendering CarPlay, but the phone's UIWindowScene stops producing frames.
  4. Unlock/foreground the phone minutes later.

A secondary, CarPlay-free variant reproduces with just background location: background the app mid-navigation then resume ~30 minutes later. Hang events (4-6 s) are reported seconds after the foreground/active transition.

Expected Result

No app hang events. The main thread was never blocked... the app was suspended / not rendering the phone scene. At minimum, hang detection should pause while the phone scene is not producing frames due to backgrounding/lock, as it does for the plain UIApplicationDidEnterBackground case.

Actual Result

App Hang Fully Blocked events whose reported duration exactly matches the backgrounded interval... we've received 22 events across 6 users with durations from 2 s up to 521 s (8.7 min). Two of the events are the fatal variant ("The user or the OS watchdog terminated your app while it blocked the main thread for at least 2000 ms"), presumably suspension followed by swipe-kill/jetsam.

Three signals show these are false positives:

1. The main-thread stack in 21/22 events is the idle run loop, not blocked work:

Thread 0 (main):
  mach_msg2_trap
  mach_msg2_internal
  mach_msg_overwrite
  mach_msg
  __CFRunLoopServiceMachPort
  __CFRunLoopRun
  _CFRunLoopRunSpecificWithOptions
  GSEventRunModal
  -[UIApplication _run]
  UIApplicationMain
  ...

(The 22nd event's main thread was waiting in CAMetalLayerPrivateNextDrawableLocked... the phone-side Metal map layer can't vend a drawable while the display is off, which is also a locked-screen symptom rather than a deadlock.)

2. Breadcrumbs bracket the "hang" with a background→foreground gap. Timeline from the 521 s event:

16:39:10 [http]          ... normal navigation traffic (TTS fetches, API calls) ...
16:46:37 [app.lifecycle] {"state":"inactive"}
16:46:37 [app.lifecycle] {"state":"background"}
16:46:39 [http]          last request completes
         --- ~7.5 minutes of silence (app suspended) ---
16:54:06 [app.lifecycle] {"state":"foreground"}
16:54:06 [app.lifecycle] {"state":"active"}
16:54:06 [ui.lifecycle]  {"screen":"CarPlayMapViewController","window":"<CPWindow: ...>", ...}
16:54:24 → event: "App hanging between 521.0 and 521.8 seconds."

The reported duration matches the suspension window, and the first breadcrumb on resume shows the active CPWindow/CarPlayMapViewController scene.

3. contexts.app.in_foreground is true on every event... we suspect the live CarPlay scene keeps UIApplication.applicationState out of .background, defeating the SDK's existing pause-on-background handling for hang tracking (the same handling that was involved in #5907's fix for background launches).

This looks like the CarPlay-scene sibling of #5907 ("App hangs v2 report very high (and invalid) durations", background-launched apps): in both cases the phone UI isn't rendering, V2 sees no frames/heartbeat, and the whole non-rendering interval is attributed to a fully-blocked main thread.

Happy to share full event JSON (stacks, breadcrumbs, contexts) from any of the 22 events privately if useful.

Are you willing to submit a PR?

TBD... but we can definitely test candidate fixes in production (our users hit this on every CarPlay drive)

Activity

  1. linear-code commented on Jul 2, 2026

    @linear-code
  2. self-assigned this
    on Jul 2, 2026
  3. philippsuess commented on Jul 2, 2026

    @philippsuess
    Contributor

    Thanks for the detailed report, @sbs44 🙏 — this is a well-diagnosed bug and the breadcrumb/stack signals were very helpful.

    ℹ️ Investigation done with Claude (Claude Code).

    TL;DR

    • Confirmed. App Hang Tracking V2 infers "main thread blocked" from a gap in rendered frames. The gap detection assumes foreground + active ⟹ the app should be rendering. That assumption breaks whenever the app is legitimately .active but the display isn't producing frames — a CarPlay scene while the phone screen is dark, or (a second customer report) an in-app phone call where the proximity sensor blanks the screen. The non-rendering interval is then misreported as a fully-blocking hang.
    • One correction to the report: contexts.app.in_foreground: true is hardcoded on every app hang event, so it isn't evidence the app was foreground during the hang.
    • We already pause detection when the app goes to background or .inactive (frames tracker pauses on willResignActive, fix(v8): Wrong Frame Delay when becoming active (#6381) #6393), so the gap is specifically: the app stays .active while rendering stops.
    • Fix is testable without CarPlay hardware or entitlements via the existing V2 test harness.
    Technical detail & fix direction

    Mechanism. When the frames tracker is running but the CADisplayLink stops, getFramesDelayObjC synthesizes a frame spanning previousFrame→now, giving delay ≈ the full window with framesContributingToDelayCount == 1 → classified fully-blocking (SentryDelayedFramesTracker.m:197-234 → SentryANRTrackerV2.m:226-234).

    Why existing guards don't cover it. Detection is gated only by app-aggregate UIApplication state — isApplicationInForeground (background flag) and the frames tracker's pause on willResignActive (.inactive). Neither fires when the app stays .active. There is no scene-level, proximity, or call/audio-session awareness.

    Two gaps for phone calls specifically:

    • A: an in-app VoIP call keeps the app .active; proximity blanks the display → no willResignActive, frames tracker never pauses → false hang.
    • B: even when the app does go .inactive, a hang already in progress isn't cleared, and the duration math excludes only .background time (SentryANRTrackerV2.m:99-139), not .inactive/paused time — so a long interruption inflates the reported duration.

    Fix direction: treat "display not rendering" as not-a-hang regardless of why (scene backgrounded, screen off), rather than relying on app-aggregate active state; and exclude .inactive/paused intervals from hang duration the same way .background is excluded.

    Repro: deterministic V2 unit test — keep isApplicationInForeground = true, keep the frames tracker running, stall the display-link wrapper past the timeout, advance time → assert a fully-blocking hang is reported today (bug) / not after the fix.

  4. Kobby-Bawuah commented on Jul 3, 2026

    @Kobby-Bawuah

    Hey team, we've got another customer hitting this that matches

    Gap A (in-app VoIP call, app stays .active while the screen blanks).

  5. removed their assignment
    on Jul 9, 2026
  6. sbs44 commented on Aug 11, 2026

    @sbs44
    Author

    Updated numbers from our production data (90-day window), since this is still reproducing on 9.19.1:

    • 43 events now (22 at filing), latest from 2026-08-10 on cocoa 9.19.1 via sentry.cocoa.react-native.
    • Durations have grown well past the original 521 s max: median 105 s, 13 events ≥ 300 s, worst 1445.7–1446.5 s (~24 min). 39 are the non-fatal variant, 4 the fatal "user or the OS watchdog terminated your app" variant.
    • Every main-thread stack is still the idle run loop (__CFRunLoopServiceMachPort → mach_msg2_trap; one each parked in kevent_id / semaphore_timedwait_trap).

    Two new signals that may help whoever picks up the fix:

    1. Main-thread breadcrumbs continue during the reported hang window. In the 1446 s event, ui.lifecycle and device.orientation breadcrumbs (both recorded from main-thread notifications) flow throughout the interval the event claims the main thread was fully blocked... the device was mounted on a moving vehicle emitting orientation changes the whole time. This directly contradicts "fully blocked" rather than just implying it from duration.

    2. The duration matches the suspension window even hours after backgrounding. An 18-min (1073 s) event: app backgrounded 17:39:30, foregrounded 19:48:56, event fired 19:52:00... the "hang" interval sits entirely inside the backgrounded period, consistent with the duration math only excluding .background time when the tracker actually observed the transition (Gap B in your analysis).

    Possibly related: our WatchdogTermination group looks like the same mechanism's fatal cousin... suspension-shaped breadcrumb gaps ending at relaunch, usable_memory ~11 GB on the most recent event (no memory pressure), across 4 unrelated releases.

    Offer stands: we can test a candidate fix in production quickly... our users hit this on every CarPlay drive, so we'd have confirmation within a day or two of shipping.

  7. lukas-ruzicka commented on Aug 11, 2026

    @lukas-ruzicka

    Independent confirmation on 9.18.0, with no CarPlay scene and no background-location entitlement — so the underlying fault appears broader than the multi-scene case in the title. Adding our fingerprint since it lines up with yours almost numerically.

    Fingerprint (production consumer iOS app, three brand targets):

    • 5,808 events / 1,175 users. The first event landed 5 days after we bumped 8.57.3 → 9.1.0 (App Hang V2 becoming the default, opt-out removed). Nothing in our app changed in that window — the instrumentation did.
    • Durations 2 s → 519 s, all in a single Sentry group, mixing Fully Blocked and Non Fully Blocked.
    • Spread evenly across iOS 17.7 → 26.6 and every device class, so it is not OS-version specific.
    • 100 % of our app-hang groups (top 30 by volume) carry the same idle main-thread stack — mach_msg2_trap → __CFRunLoopServiceMachPort → __CFRunLoopRun → GSEventRunModal → -[UIApplication _run], culprit resolving to our App.main entry point. Not one group has an app frame on the main thread, which is what a genuine block would produce.
    • The fatal variant is present too (~58 events/30 d): "The user or the OS watchdog terminated your app while it blocked the main thread for at least 2000 ms."

    One extra signal that pins the arithmetic: every event's min/max spread is exactly 0.8 s — 3.6–4.4, 24.7–25.5, 324.0–324.8, 518.2–519.0 — i.e. 2 × sleepInterval with appHangTimeoutInterval at its 2 s default. So the duration comes from appHangDurationNanos ± sleepIntervalInNanos in SentryANRTrackerV2.detectANRs, and because anrStopped only fires once getFramesDelay falls back under appHangStoppedFrameDelayThreshold, the reported duration is precisely the interval during which no frames were produced — not an interval during which the main thread was blocked.

    Where the state gets stuck (read at 9.18.0; the tracker is byte-identical in 9.21.0):

    • Detection rests solely on framesContributingToDelayCount == 1 over the timeout window.
    • The suppression gates observe only didEnterBackground / didBecomeActive (SentryThreadsafeApplication, sentrycrashstate) plus willResignActive → SentryFramesTracker.pause().
    • If that pause() is missed for any reason before rendering stops, isRunning stays true and SentryDelayedFramesTracker.previousFrameSystemTimestamp freezes, so the synthesised "current frame" in getFramesDelayObjC spans every subsequent query window.
    • unpause()'s guard !isRunning else { return } then lets that state survive the round trip — nothing self-heals until frames resume, at which point the entire away interval is emitted as one hang.
    • accumulatedBackgroundTime cannot compensate, since it is keyed on the same notification-derived foreground flag.

    Suggestion: gate detection on frames having actually been observed recently (or on scene-level activity), rather than on cached app-level notification state. It would also help to re-expose the V1/V2 choice that 9.0 removed — today the only lever is enableAppHangTracking = false, which discards genuine hang detection along with the noise.

  8. mberbet commented on Aug 15, 2026

    @mberbet

    Fourth independent fingerprint, from a Capacitor (WKWebView hybrid) fitness app — no CarPlay, no VoIP, no background-location entitlement. It matches the pattern in this thread almost exactly, adds a hybrid-app data point, and includes one clean breadcrumb timeline of a rapid background/foreground-flapping variant.

    Environment: sentry-cocoa 9.19.0 via @sentry/capacitor 4.2.0, appHangTimeoutInterval = 2, production iOS.

    Fingerprint (90-day window):

    • 41 events / ~30 users in a single group (46 lifetime since late April), spread evenly across 20+ releases from May to August — no correlation with any app change.
    • Durations 2.0 s → 493.2 s, median ~21 s. 35 Fully Blocked, 6 Non Fully Blocked. No fatal variant in this group so far.
    • Every sampled main-thread stack is the idle run loop (mach_msg2_trap → __CFRunLoopServiceMachPort → __CFRunLoopRun → GSEventRunModal → -[UIApplication _run] → UIApplicationMain). Not one app frame on the main thread.
    • Independent confirmation of @lukas-ruzicka's arithmetic: all 41 events have a min/max spread of exactly 0.8 s (2.1–2.9, 29.9–30.7, 199.9–200.7, 492.4–493.2, …), consistent with 2 × sleepInterval at the 2 s timeout.

    Timeline of a 29.9–30.7 s Fully Blocked event (13 Aug, iPhone17,3, 644 MB free — no memory pressure), showing the rapid-flapping variant:

    18:02:32 [app.lifecycle] {"state":"background"}
    18:02:32 [app.lifecycle] App → foreground
    18:02:33 [app.lifecycle] App → background
    18:02:33 [console]       background flush + Live Activity/watch sync   ← app doing normal work
    18:02:33 [http]          2 requests complete
    18:02:40 [app.lifecycle] {"state":"foreground"}
    18:02:41 [app.lifecycle] {"state":"active"}
    18:02:41 [console]       Live Activity/watch sync                       ← responsive immediately
    18:02:xx → event: "App hanging between 29.9 and 30.7 seconds."
    

    The app transitions background↔foreground twice within one second, performs real work on each transition (our background flush and Live Activity updates complete normally), and is responsive immediately on resume — yet a ~30 s fully-blocking hang is reported. This looks consistent with Gap B from the analysis above: an in-progress "hang" isn't cleared across rapid transitions, and the excluded-time bookkeeping misses part of the away interval.

    One contextual note on why some app categories may be hit unusually often: our users flip in and out of the app constantly mid-workout (rest timers, music, notifications), so lifecycle transitions are far more frequent than in a typical app session. Happy to share full event JSON (stacks, breadcrumbs, contexts) privately if useful.

  9. sbs44 commented on Aug 21, 2026

    @sbs44
    Author

    @NinjaLikesCheez @philipphofmann – checking in on this one. It's been ~7 weeks since filing and ~5 since it was marked high priority; the thread now has four independent fingerprints (CarPlay, VoIP, lifecycle-flapping, and a Capacitor hybrid app) with thousands of events between them.

    The frustrating problem on our side: these fire almost daily, and since the only available lever is enableAppHangTracking = false, we either drown in false "fully blocked" alerts or lose real hang detection entirely.

    I'm willing to open a PR. Between the fix direction in Philipp's comment and the state-machine analysis from @lukas-ruzicka, the shape seems clear... gate detection on frames actually having been observed rather than cached notification state, and exclude .inactive/paused intervals from the duration math.

    ... but before I start: would you accept a community PR here, and does that direction look right to you (or is an internal fix already underway that I'd be duplicating)? I have 3 merged PRs across getsentry/sentry-javascript and getsentry/sentry-react-native, but this would be my first rodeo with sentry-cocoa.

  10. mberbet commented on Aug 21, 2026

    @mberbet

    Would second accepting a community PR here — the fix direction matches what all four fingerprints in this thread show (idle main-thread stacks + reported duration ≈ the interruption time, not actual blocked time).

    Happy to help validate from the Capacitor hybrid side: we can pin a branch build via SPM against our fingerprint and exercise the lifecycle-flapping repro locally, and since our group fires steadily in production (~0.5 events/day across 20+ releases), we can confirm the fix empirically within days of it shipping in a release.

  11. added a commit that references this issue on Aug 24, 2026
    4edfd9b
  12. NinjaLikesCheez commented on Aug 24, 2026

    @NinjaLikesCheez
    Member

    ... but before I start: would you accept a community PR here

    Absolutely and thank you for being willing to get this across the line! I'd like to rope in @philprime here too since he's looked at App Hang Tracking recently and knows more about that subsystem than I do.

    To add some context, we have requested a CarPlay entitlement from Apple but haven't heard back yet - although from reading Philipp's comment it may not be needed.

  13. tlapuste-a commented on Sep 9, 2026

    @tlapuste-a

    Fifth fingerprint, and it is the in-app call case from the Gap A description. VoIP app, sentry-cocoa 9.7.0 via @sentry/react-native 8.5.0, App Hangs V2 at the 2 s default.

    Repro on our app takes ten seconds: start a call, hold the phone to your ear until the proximity sensor blanks the screen, lower it. The SDK reports a fully-blocking hang for the time the screen was dark. My last run on iOS 18.7.8 produced "App hanging between 12.1 and 12.9 seconds" with the main thread in mach_msg2_trap under __CFRunLoopRun.

    In prod over the last 90 days, our largest error issues in our iOS project have been the following 2 hang groups, at about 6x the volume of the next non-hang issue between them:

    p50 p90 max
    group A 10.7 s 161 s 496 s
    group B 28.1 s 149 s 381 s

    ^percentiles coming from the latest 100 events of each group, with every min/max spread being 0.8s, and every main-thread stack being the idle run loop. Releases span five versions and iOS 17.6 to 26.6 with no correlation to either. From what we've seen, the tail past a minute lines up with device lock and suspension rather than calls, so that also overlaps with the suspension reports in #8899.

    We gave in and turned app hang tracking off this week to stop the noise... but it cost us nothing, since not one of our hang groups has ever shown an app frame on the main thread.

    One question @NinjaLikesCheez, since it sounds based on #8899 (comment) that the plan is to replace V2 with MetricKit hang reports in v10, is #8840 looking to be a candidate for a 9.x release or is v10 the more likely fix path? If it's v10, a note here would let the rest of this thread stop waiting on the PR.

  14. philprime commented on Oct 16, 2026

    @philprime
    Member

    Thank you for reporting this issue.

    We are aware that our custom app hang tracking solution has some known issues and we're recommending to replace it with MetricKit hang diagnostic reporting instead. App Hangs V1/V2 will be removed in the next major release.

    Additionally, we're considering adding a custom hitch detection feature. See #9356 for details.

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

Metadata

Metadata

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions