An OOM (Out of Memory) crash is simply when the app runs out of space to think — and shuts down. Every byte counts when your users span across Bharat. At Meesho, we build for everyone in India, including people using budget phones with limited memory. When an app runs out of memory, it crashes. And crashes mean lost users.
So when we started noticing our app was crashing after just a few minutes of normal use, we knew we had a real problem. This is the story of how we tracked it down, fixed it, and made Meesho faster for millions of users.
The Context: Scale and Stakes at Meesho
Meesho is one of India’s largest social commerce platforms, serving millions of daily active users. During major sale events, our Android app sees a huge surge in app activity as users browse endlessly, compare products, jump between categories, and use the app for long periods of time. That’s a stress test for any mobile runtime.
But on low-end devices, it becomes more critical. The user’s phone is the final infra. If our app doesn’t behave nicely, the OS will punish it:
- Force-close parts of the app to free up memory
- Wipe out screens/activities the user visited earlier
- Constantly pause the app to clean up memory — making it feel slow and unresponsive
- And worst of all — crash entirely.
During Sale Days, all of these issues tend to spike the most.
Each OOM meant lost user sessions, interrupted shopping journeys and a dent in user trust. So we decided to go leak hunting.
The Investigation Begins
When your app shows high memory usage, the instinct is to optimize images, caches, lists etc.
But in our case, we saw a gradual memory buildup — one that didn’t free up even after navigation or activity destruction. Classic symptoms of memory leaks.
Hence, we wanted to detect leaks in production, in real time, without degrading user experience.
Step 1 — Understanding Memory Metrics
We started simple, by tracking our app’s memory footprint using built-in Android APIs:

From these, we can capture:
- PSS (Proportional Set Size) → shared memory adjusted usage
- USS (Unique Set Size) → memory exclusive to our process
- RSS (Resident Set Size) → total RAM occupied
But, these gave us numbers about memory consumption, but we wanted to know more about when the garbage collector (GC) ran, how often, and whether it was able to reclaim destroyed objects to track down leaks.
The Search for Better Metrics
While exploring open-source tools, we looked into Android internals to capture alloc metrics.
We found legacy APIs like to track memory consumption between two points in code, even at thread level:

…but they were deprecated (some in API 18 and some in 23) long ago. It was a dead end.
Then came the eureka moment — buried deep in Android API level 23+, we discovered:
Debug.getRuntimeStat(String statName)
You can find the available range of statistics names and their definitions here and more details about all above APIs here.

This tiny API opened a treasure chest of runtime metrics:

Finally, we could track GC frequency directly from runtime, without intrusive hooks or performance overhead.
Connecting the Dots: GC Patterns & Leaks
Once we had real-time GC data, a thought struck us:
“If the system GC is running frequently, certain activities still aren’t reclaimed… they must be leaking!”
We have an internal leak tracker in production that holds weak references to activities using a lifecycle listener. Upon the onDestroy of an activity, it forces garbage collection and checks if the activity is still reachable; if it is, it reports it as a leak.
But forcing GC in production causes jank, so we wanted a non-intrusive approach:
- No forced GC
- Passive observation of system GC patterns
- Weak references to detect uncollected objects
This was our turning point.
Leak Detection in Production
We now had our runtime statistics, the gc-count and blocking-gc-count numbers that gave us a real-time window into how often the Android runtime was trying to reclaim memory.
But numbers alone aren’t enough. We needed a way to correlate them with activity lifecycles; the actual entities living (and dying, or rather not dying) inside our app.That’s when we began designing our in-house Leak Tracker, our lightweight, production-safe leak detection framework.
Activity Tracking
The idea was simple:
Whenever an Activity is created, we store its weak reference to an “active” stack and remove it when destroyed.
If an activity persists in memory, even after being destroyed and garbage collection has run repeatedly, it constitutes a leak.
We defined a small holder called ActivityRef.
And maintained two lists (weak references):
- referenceList: all seen activities
- userActivityStack: currently active activities
These lists were kept thread-safe using CopyOnWriteArrayList, ensuring minimal locking overhead.
Lifecycle Hooks
We plugged into the Application.ActivityLifecycleCallbacks to register every activity at creation and destruction:

The startTracking() method captured the current GC count from Debug.getRuntimeStat() and scheduled a background runnable to re-check later.
This delay (about 5 seconds) gave the system enough breathing space to perform a few GC cycles before we judged whether the activity was gone.
Detecting Leaks via GC Delta
Every few seconds, we ran:

If, after multiple GC cycles (up to GC_THRESHOLD), an activity’s weak reference still pointed to a valid instance even though it had been destroyed — we declared it leaking.
We tried with different variations and combinations of GC_THRESHOLD and delay time, to make leak identification more accurate.
This approach had zero forced GC calls, minimal CPU overhead, and near-real-time insight into memory health.
The Leak Fixer — Healing Leaks on the Fly
Once we identified leaks, we wanted to do more than just log them.
We wanted to mitigate them instantly — to keep the app stable even before engineers pushed fixes.
Here enters the Leak Fixer.
It attempted to clean up leaked activities by nullifying all references and clearing their views using reflection.
We replaced the leaked activity’s layout with a dummy XML and nulled its fields to release bitmaps, adapters, and other heavy objects
And then we used reflection to sweep fields clean, as per below code:

All of this reflection based code ran on a background thread to avoid UI jank .
Observability & Insights
Detecting and fixing leaks was half the battle; observing them at scale was the other half.
We piped data into our internal dashboards to track:
- Which activities leaked most often
- How long leaks persisted
- GC call frequency per session
- Heap growth patterns during sale events
This observability loop turned leak tracking into a regression-cycle ritual — engineers would pick top-leaking screens, patch them, and measure improvements over the next release.
Over time, OOM crash rates plummeted, and user session lengths improved.
Results
This is how our OOM trend evolved over time. We began experimenting in late June with a small cohort, closely monitoring stability and performance. As confidence grew, we gradually expanded the rollout. With wider app adoption, our OOM percentage, over a span of 3 months of adopting this optimization:
- Over 50 - 55% reduction on BAU (Business-As-Usual) days
- Over 60 - 65% reduction during high-traffic Sale spikes
Meesho’ Biggest Yearly (Diwali) sale improvement:
The first day of our 2025 MBS i.e. Mega Blockbuster Sale (in Sept) delivered an 80 - 85% reduction in OOM crashes compared to that of 2024 MBS first day sale (in Sept).

Learnings
A few things that might be useful if you're tackling something similar:
- Distinguish leaks from high usage early. Gradually rising memory that doesn't recover even after activity onDestroy is almost always a leak. The optimisation playbook — image compression, cache sizing — won't fix it.
- Debug.getRuntimeStat() is underused. It's been available since API 23 and provides direct ART insight. Most teams reach for heap dumps or forced GC instead.
- Treat mitigation and detection as separate concerns. Being able to reduce a leak's footprint in production, independently of shipping a fix, meaningfully reduces user impact.
- Tune thresholds per app. GC_THRESHOLD and TIME_INTERVAL are sensitive to navigation depth and session length. What works for our app won't transfer directly to other apps.
The Bigger Picture
Building a memory-resilient Android app at Meesho wasn’t about finding one magic bullet — it was about layered defense:
- LeakTracker for detection in production
- Leak Fixer for mitigation
- Observability for feedback loops
All while ensuring zero user impact, even on budget devices.
We started this journey to reduce OOMs — and ended up with a framework that keeps our users’ experience smooth across Bharat.
Conclusion
LeakTracker began as a small experiment to understand memory behaviour, but soon became one of the silent guardians of Meesho’s Android performance stack.
Today, it monitors millions of user sessions — ensuring smooth journeys even on the smallest of phones.
Shoutout
Big thanks to Kavis Khandelwal and Vibin Reddy for always guiding and mentoring me.

