Setting Up Watchdog Termination Monitoring for iOS Apps
Watchdog is an iOS system mechanism that forcibly terminates an app if it stops responding for too long. Unlike a normal crash, this is not an exception or signal. MetricKit calls such terminations MXHangDiagnostic, and ProcessInfo.processInfo.reason contains no trace — just the fact of termination.
Starting with iOS 13, Watchdog activates when the main thread hangs for > 8 seconds in foreground. On iOS 16+, the threshold dropped to ~4 seconds in some scenarios. Users simply see the app close without dialogs.
Why This is Hard to Monitor
Firebase Crashlytics does not register Watchdog Terminations — they don't pass through NSSetUncaughtExceptionHandler and don't generate a signal. Crashlytics only sees standard crashes.
Sentry from version 8.0 can detect Watchdog via SentryWatchdogTerminationTracker, but only if the SDK lived in memory before termination — on cold start after Watchdog, Sentry sees it through flags in UserDefaults.
Apple's MetricKit provides accurate data, but with delay: diagnostic payloads arrive once daily.
Detection via MetricKit
import MetricKit
class MetricsManager: NSObject, MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
// Performance metrics
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
if let hangs = payload.hangDiagnostics, !hangs.isEmpty {
hangs.forEach { hang in
// hang.callStackTree — call stack tree at hang moment
reportHangToServer(hang)
}
}
}
}
}
// Registration
MXMetricManager.shared.add(MetricsManager())
MXHangDiagnostic.callStackTree contains the symbolicated (if dSYM available) main thread stack at the hang moment. This is the only way to know exactly which method hung.
Custom Watchdog Detector
If waiting for MetricKit isn't possible, build a detector manually:
final class WatchdogDetector {
private let queue = DispatchQueue(label: "watchdog.monitor", qos: .utility)
private var pingTime: Date = Date()
private let threshold: TimeInterval = 3.0
func start() {
scheduleMainThreadPing()
scheduleBackgroundCheck()
}
private func scheduleMainThreadPing() {
DispatchQueue.main.async { [weak self] in
self?.pingTime = Date()
self?.scheduleMainThreadPing() // schedule next ping
}
}
private func scheduleBackgroundCheck() {
queue.asyncAfter(deadline: .now() + 1.0) { [weak self] in
guard let self = self else { return }
let elapsed = Date().timeIntervalSince(self.pingTime)
if elapsed > self.threshold {
// Record main thread hang
self.captureHang(duration: elapsed)
}
self.scheduleBackgroundCheck()
}
}
private func captureHang(duration: TimeInterval) {
// Thread.callStackSymbols — only current thread
// For main thread: use BSBacktraceLogger or PLCrashReporter
SentrySDK.capture(error: NSError(
domain: "WatchdogHang",
code: Int(duration * 1000),
userInfo: [NSLocalizedDescriptionKey: "Main thread hung for \(duration)s"]
))
}
}
Caution: Thread.callStackSymbols captures the stack of the current thread (background), not main. To capture another thread's stack, use BSBacktraceLogger (third-party library) or PLCrashReporter.
Sentry Watchdog Tracking
SentrySDK.start { options in
options.dsn = "https://[email protected]/project"
options.enableWatchdogTerminationTracking = true
options.appHangTimeoutInterval = 3.0 // App Hang threshold in seconds
}
Sentry stores a sentryWatchdogTermination flag in UserDefaults on every startup. If the flag is set on next launch and the last termination wasn't clean — a Watchdog Termination is recorded. Not perfectly accurate (false positives from force-kill via Xcode), but works in production.
Typical Watchdog Termination Sources
- Synchronous CoreData fetch in
viewDidLoadon the main thread -
DispatchSemaphore.wait()orDispatchGroup.wait()without timeout on main thread - Deadlock between
@MainActorand synchronous Swift Concurrency code - Heavy JSON decode in
URLSession.dataTaskcompletion without dispatch to background queue
What We Do
- Enable
MetricKitsubscriber to receiveMXHangDiagnostic - Connect Sentry
enableWatchdogTerminationTrackingfor real-time detection - When needed, implement custom Watchdog detector for lower threshold
- Set up alerts for Watchdog Termination Rate growth
- Analyze
callStackTreeto identify specific culprits
Timeline
Basic setup via Sentry: 4–8 hours. MetricKit integration with diagnostic delivery: 1–2 days. Pricing is calculated individually.







