Hang Detection
Hang Detection
The Embrace SDK can monitor your application's main thread for app hangs, providing visibility into UI freezes and unresponsive behavior that can frustrate users.
- Requires SDK 6.19.0 or later, which introduced the frame-rate–based hang detector.
- Available on iOS. Hang detection relies on
CADisplayLink, so it is not supported on every Apple platform the SDK builds for. - Opt-in. The service is not installed by default — see Enabling Hang Detection.
What are Hangs?
A hang occurs when your app's main thread (UI thread) is blocked for too long, preventing the app from responding to user interactions. During a hang, your app may appear frozen—buttons don't respond, animations stop, and the UI becomes unresponsive.
Common causes of hangs include:
- Performing heavy computations on the main thread
- Synchronous network requests
- Large file I/O operations
- Inefficient database queries
- Deadlocks or race conditions
- Slow third-party SDK initialization
Even hangs as short as 250 milliseconds can be noticeable to users and negatively impact the user experience.
For more information about understanding and improving hangs in iOS apps, see Apple's documentation:
How Hang Detection Works
The SDK observes frame delivery on the main thread using CADisplayLink. On each frame it compares the actual delivery time against the system's own committed schedule for that frame (the previous tick's targetTimestamp). When a frame arrives more than 249 milliseconds late (Apple's recommended threshold), the delay is reported as a hang with:
- Start time and duration of the hang
- A stack trace of the main thread, to identify the blocking code
- Associated session and user context
Comparing against targetTimestamp rather than a fixed frame duration makes this approach safe under ProMotion, Low Power Mode, and preferredFrameRateRange transitions — none of them produce false positives. CADisplayLink also pauses in the background, so app suspension gaps are never reported as hangs.
The SDK becomes aware of a hang only once the delayed frame is finally delivered — that is, after the main thread has already unblocked. Two consequences:
- A hang that never recovers (the OS terminates the app while the main thread is still blocked) produces no
emb-thread-blockagespan. Those appear as watchdog terminations instead, captured via MetricKit. - The stack trace is captured immediately after recovery, so it reflects the main thread at the end of the hang, not a sampled timeline across it.
Key Benefits
- Detect UI freezes and unresponsive behavior
- Identify code causing main thread blockages
- Track hang frequency and duration
Enabling Hang Detection
Since the frame-rate–based hang detector was introduced in 6.19.0, hang detection is opt-in. It is not installed by EmbraceIO.CaptureServicesOptions.default() and is not added by CaptureServiceBuilder.addDefaults(). You must register the service explicitly when you set up the SDK.
- EmbraceIO
- Embrace
import EmbraceIO
let captureServices = CaptureServicesOptionsBuilder()
.addDefaults()
.addHangCaptureService()
.build()
let options = EmbraceIO.Options.withAppId(
"YOUR_APP_ID",
captureServices: captureServices
)
do {
try EmbraceIO.setup(options: options)
try EmbraceIO.shared.start()
} catch {
print("Failed to set up Embrace: \(error)")
}
import EmbraceIO
import EmbraceCore
import EmbraceCrash
let services = CaptureServiceBuilder()
.addDefaults()
.add(HangCaptureService())
.build()
let options = Embrace.Options(
appId: "YOUR_APP_ID",
captureServices: services,
crashReporter: KSCrashReporter()
)
do {
try Embrace.setup(options: options)
try Embrace.client?.start()
} catch {
print("Failed to set up Embrace: \(error)")
}
crashReporter has no default value on this initializer. Passing an explicit CrashReporter — normally KSCrashReporter() — is required whenever you supply your own captureServices array.
Configuration Options
HangLimits
HangLimits controls the behavior of the detector:
| Property | Default | Description |
|---|---|---|
hangThreshold | 0.249 | Minimum frame delay, in seconds, reported as a hang. Based on Apple's recommended threshold. |
hangPerSession | 20 | Maximum hangs captured per session. Set to 0 to disable hang detection entirely — the frame-rate monitor is never created. |
reportsWatchdogEvents | false | Reserved for internal use. Leave at the default. |
Once hangPerSession hangs have been captured in a session, further hangs are dropped and the SDK logs Dropping hang due to surpassing limit. The count resets when a new session starts, and the total is attached to the session as the emb-thread-blockage metadata property.
HangCaptureService(limits:) accepts a HangLimits, but the SDK overwrites the service's limits with the active configuration's hangLimits during setup, and again on every configuration refresh.
When you initialize with an appId, hang limits therefore always come from Embrace's remote configuration. Passing HangLimits locally has no effect in that setup. Contact Embrace support if you need different thresholds or a higher per-session cap for your app.
A local HangLimits takes effect only in the export-only setup, where you supply your own EmbraceConfigurable. The simplest approach is to mutate DefaultConfig:
import EmbraceIO
import EmbraceConfiguration
let config = DefaultConfig()
config.hangLimits = HangLimits(
hangThreshold: 0.249, // seconds of frame delay that counts as a hang
hangPerSession: 20 // 0 disables hang detection
)
let options = EmbraceIO.Options.withLocalConfiguration(
config,
captureServices: CaptureServicesOptionsBuilder()
.addDefaults()
.addHangCaptureService()
.build(),
otel: EmbraceIO.OTelOptions(
spanExporter: mySpanExporter,
logExporter: myLogExporter
)
)
do {
try EmbraceIO.setup(options: options)
try EmbraceIO.shared.start()
} catch {
print("Failed to set up Embrace: \(error)")
}
When attached to a debugger, hang detection is off. If you wish to enable it, set the EMBAllowWatchdogInDebugger environment variable to 1.
Data Captured
For each captured hang, the SDK records:
- Start time and duration
- One stack trace of the main thread, captured as a span event immediately after the hang recovers
- Session context
Upload dSYM files to see symbolicated stack traces. See dSYM Upload Guide.
How Hangs Appear in OpenTelemetry
Each hang is reported as an OpenTelemetry span named emb-thread-blockage, with emb.type set to perf.thread_blockage. The span's start and end times bound the hang, so its duration is the measured frame delay.
The main-thread stack trace is attached as a span event named thread_blockage_sample, carrying the encoded stack along with frame_count and sample_overhead attributes.
Integration with Other Features
Hangs are automatically correlated with:
- Sessions and user identification
- Active views
- Network requests
Non-recovering hangs are not represented by these spans. If the OS terminates the app while the main thread is still blocked, the event surfaces as a watchdog termination captured through MetricKit.
Best Practices
Default Settings
The defaults (hangThreshold: 0.249, hangPerSession: 20) are tuned for production and add minimal overhead: one CADisplayLink callback per frame, and a single stack capture per reported hang.
Common Hang Sources
Common code patterns that cause hangs:
// Bad: Synchronous network request on main thread
let data = try Data(contentsOf: url) // Blocks until download completes
// Good: Async network request
URLSession.shared.dataTask(with: url) { data, response, error in
// Handle response on background thread
}.resume()
// Bad: Heavy computation on main thread
let result = processLargeDataset(data) // Blocks UI
// Good: Background computation
DispatchQueue.global(qos: .userInitiated).async {
let result = processLargeDataset(data)
DispatchQueue.main.async {
// Update UI with result
}
}
// Bad: Synchronous database query on main thread
let users = try context.fetch(fetchRequest) // May block for large result sets
// Good: Async database query
context.perform {
let users = try? context.fetch(fetchRequest)
// Process results on background context
}
Reducing Hangs
- Move heavy work off the main thread
- Use async/await or GCD for long-running operations
- Optimize rendering and view hierarchy complexity
- Profile with Instruments to identify bottlenecks
Disabling Hang Detection
Hang detection is off by default. To turn it off after enabling it, remove the addHangCaptureService() / add(HangCaptureService()) call from your setup.
It can also be disabled remotely by setting hangPerSession to 0, which prevents the frame-rate monitor from being created. In an appId setup that value is controlled by Embrace's remote configuration, so contact Embrace support if you need the feature switched off without shipping a new build.
Troubleshooting
Not Seeing Hang Data
Work through these in order — the first two account for most cases:
- Detach the debugger. Hang detection disables itself whenever a debugger is attached, so running from Xcode produces no hangs. Either test on a device launched outside Xcode, or set the
EMBAllowWatchdogInDebuggerenvironment variable to1. The SDK logs a warning when it disables itself for this reason:[FrameRateMonitor] Disabled because a debugger is attached. - Confirm the service was registered.
HangCaptureServiceis not part ofaddDefaults()orEmbraceIO.CaptureServicesOptions.default(). Verify your setup callsaddHangCaptureService()oradd(HangCaptureService()). - Verify
hangPerSession > 0. With anappId, this value comes from remote configuration. If it is0, the monitor is never created and the SDK captures nothing without reporting an error — contact Embrace support to confirm the value for your app. - Check you are on SDK 6.19.0 or later, and running on iOS.
- Check the per-session cap. After
hangPerSessionhangs, later hangs in the same session are dropped, and the SDK logsDropping hang due to surpassing limit. - Reproduce deliberately with
Thread.sleep(forTimeInterval: 0.5)on the main thread. Note that the hang must recover for a span to be produced. - Confirm sessions are uploading successfully.
Stack Traces Not Symbolicated
Ensure dSYM files are uploaded for your app version. See dSYM Upload Guide.
Related Documentation
- Performance Monitoring - Manual performance instrumentation
- Session Reporting - Understanding session data
- dSYM Upload - Symbolication setup
- Configuration Options - Complete SDK configuration
- View Tracking - Correlate hangs with specific views