How do you detect memory leaks in production?
Why production detection is fundamentally different from dev-time detection
In development, you attach Instruments’ Leaks/Allocations tool or Xcode’s Memory Graph Debugger and directly inspect the object graph. In production, you have none of that — you’re inferring “something is leaking” from symptoms: rising memory usage, increased jetsam/OOM crash rates, degraded performance over a session — and then working backward to find the cause.
1. Memory usage telemetry — the primary signal
func logMemoryFootprint(context: String) {
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.size) / 4
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
if result == KERN_SUCCESS {
let usedMB = Double(info.phys_footprint) / 1024 / 1024
Analytics.log("memory_footprint", properties: ["mb": usedMB, "context": context])
}
}
Sending this at key checkpoints lets you build a picture of memory trend per flow across your whole user base.
2. Jetsam/OOM crash reports — the clearest downstream signal
On iOS, memory leaks that get bad enough result in the OS killing the app (a “jetsam” event) rather than a traditional crash. Crash reporting tools that distinguish OOM terminations from actual crashes are essential — a rising OOM rate is one of the strongest production leak signals.
MetricKit specifically is worth knowing by name — Apple’s MXMetricPayload/MXDiagnosticPayload APIs deliver aggregated memory and diagnostic data from real user devices, without needing a third-party SDK.
3. Session-length correlation
A leak often manifests as “memory grows the longer the app stays open,” so bucketing memory footprint (or OOM rate) against session duration or number of screen transitions is a useful analysis.
4. Feature-flag/version correlation to isolate the source
Once you see a memory/OOM signal, correlating it against app version and active feature flags narrows down when the leak was introduced — letting you go straight to the relevant diff rather than searching the whole app.
5. Automated detection in CI/QA before it ever reaches production — the actual prevention layer
func trackForMemoryLeaks(_ instance: AnyObject, file: StaticString = #filePath, line: UInt = #line) {
addTeardownBlock { [weak instance] in
XCTAssertNil(instance, "Instance should have been deallocated. Potential memory leak.", file: file, line: line)
}
}
Also: Instruments’ Leaks tool run automated in CI against key user flows, and Xcode’s Memory Graph Debugger during manual QA passes on new features.
6. Common leak patterns worth explicitly checking for
- Closures capturing
selfstrongly in a context that outlives the intended scope. - Delegate properties declared
strong/non-weak. - NotificationCenter observers never removed.
- Timers retaining their target strongly.
- Combine subscriptions/
AnyCancellablenever stored or cancelled.
7. What production detection genuinely cannot give you: root cause
Production telemetry tells you that something is leaking and roughly where, but essentially never tells you which specific object/closure/retain cycle is responsible — that last step almost always requires reproducing the suspected flow locally with Instruments once you have a strong enough hypothesis.
Interview-ready summary
| Signal | What it tells you |
|---|---|
| Memory footprint telemetry (custom logging) | Trend per screen/flow across real user population |
| MetricKit / OOM-classified crash reports | Strongest signal something is leaking badly enough to kill the app |
| Session-length correlation | Whether the issue is accumulation-over-time vs. a fixed cost |
| Version/feature-flag correlation | Narrows down when the leak was introduced |
| CI-run Instruments / XCTest leak assertions | Prevention — catches leaks before they ship |
| Manual Memory Graph Debugger passes | Root-cause diagnosis, once you have a hypothesis |
One-liner if pressed: “In production I’m working from symptoms, not direct object-graph inspection — memory footprint telemetry per screen, MetricKit/OOM-classified crash data, and correlating rising memory or termination rates against app version and session length to narrow down where and when a leak was introduced. That narrows the search, but actually finding the retain cycle almost always means reproducing the flow locally with Instruments’ Memory Graph Debugger — the real prevention is catching leaks earlier, via XCTest deallocation assertions and Instruments passes in CI before release.”