Memory leaks are among the most frustrating bugs to diagnose: your app is slow, your fan is spinning, and you’re not sure which process is the culprit or why. This guide walks through a systematic approach to finding memory leaks on macOS using ProcXray.
Quick Answer
To debug memory leaks on macOS, first confirm steady non-recovering memory growth, then identify the leaking process, validate launch context and environment, and correlate with file descriptor or library signals. Use ProcXray for rapid triage and platform profilers for code-level root cause.
Recognizing the Symptoms
Before reaching for a tool, confirm you have a memory leak rather than legitimate high usage:
- Steadily increasing memory over time (not just a spike during a task)
- Memory doesn’t drop after the app becomes idle
- System swap usage grows even though you’ve closed other apps
- The process never frees memory even after the work that triggered growth is complete
Step 1: Identify the Leaking Process
Open ProcXray and sort by Memory (descending). Look for:
- A process whose memory column keeps climbing
- A process with unexpectedly high memory for what it does (e.g., a background daemon using 2 GB)
ProcXray’s real-time memory chart in the detail panel’s History tab updates continuously. Pin a suspect process and watch the trend line — a leak shows as a steady upward slope that never flattens.
Step 2: Check What the Process Actually Is
Click the suspect process. In the General tab you’ll find:
- Full executable path (useful when the process name is ambiguous, like
nodeorpython3) - Command-line arguments (tells you which script or server is running)
- Working directory
- Parent process (tells you what launched it)
A background node process eating 3 GB is worth nothing without knowing it’s node /Users/me/myapp/server.js.
Step 3: Inspect Environment Variables
Switch to the Environment tab. Memory leaks in Node.js, Python, or Ruby apps are sometimes caused by environment misconfiguration:
NODE_ENV=developmentenabling heavy debug middleware in production- An ORM configured for verbose logging that keeps query objects in memory
- A cache library configured with no eviction policy
Copy the environment as JSON and review any cache sizes, pool limits, or debug flags.
Step 4: Check Open File Descriptors
Memory leaks in servers are often paired with file descriptor leaks — the process opens files, sockets, or pipes and never closes them. Switch to the Connections tab in ProcXray to see:
- All open files
- All listening ports
- All active network connections
A server that has 10,000 open file descriptors when it should have 50 is a strong signal of a related resource leak.
Step 5: Check Loaded Libraries
Sometimes the leak is in a native library. ProcXray’s Modules tab lists every dynamic library the process has loaded. Cross-reference this with your app’s known dependencies — an unexpected library version or a library that shouldn’t be there at all can explain unusual behavior.
Step 6: Reproduce and Confirm
Once you have a suspect (a specific process + code path), reproduce the leak deliberately:
- Note the process’s current memory in ProcXray
- Trigger the operation you suspect causes the leak (e.g., upload a file, run a query, call an API)
- Watch the memory trend in ProcXray
- Wait for the app to become idle
- If memory doesn’t return to baseline, you’ve confirmed the leak path
Step 7: Deep Dive with Platform Tools
ProcXray gives you the what and where. For the why inside your code, use platform-specific tools:
For native Swift/Objective-C apps:
leaks <PID> # Apple's built-in leak detector
malloc_history <PID> # Allocation history
Or use Xcode’s Memory Graph Debugger for a visual call-tree.
For Node.js:
node --inspect server.js
# Then use Chrome DevTools Memory tab or clinic.js
For Python:
from memory_profiler import profile
@profile
def my_function():
...
Common Culprits
| Cause | Signal |
|---|---|
| Unbounded cache | Memory grows proportional to requests |
| Event listener not removed | Memory grows with UI interactions |
| Native library leak | Modules tab shows version mismatch |
| File descriptors not closed | Connections tab shows thousands of open files |
| Retained DOM nodes | Browser process grows despite navigating away |
Summary
- Sort by memory in ProcXray and identify the growing process
- Use the General tab to confirm what the process is and how it was launched
- Check environment variables for misconfiguration
- Check the Connections tab for file descriptor leaks
- Reproduce the leak path
- Hand off to Xcode Memory Debugger, clinic.js, or memory_profiler for code-level analysis
Download ProcXray → to start your investigation — free, macOS Sonoma+.
FAQ
How do I tell a memory leak from a normal memory spike?
A spike often stabilizes or drops after workload completes. A leak usually appears as persistent growth across repeated operations, without returning to baseline during idle periods.
Why check file descriptors when debugging memory leaks?
Resource leaks often co-occur. A process that continuously opens sockets or files without closing them can show both descriptor growth and memory pressure.
Which tool should I use after I find the leaking process?
Use language- or platform-specific profilers: Xcode Memory Graph for Swift/Objective-C, DevTools/clinic.js for Node.js, and memory_profiler or tracemalloc workflows for Python.