Memory leaks cost you money. Literally.
As mobile devs, we often don’t care how much memory our app uses — as long as we don’t hit the dreaded OutOfMemory
exception.
Let’s be honest: we do care as professionals, but if the app consumes 100 MB more than it should, it doesn’t hurt us. The app runs on our users’ phones — we’re not the ones paying for the extra RAM.
Well… I recently learned to care the hard way with my Telegram Finance Bot.
Yesterday, Railway messaged me that my bill doubled over the last two weeks.
I got curious - what’s causing this? Memory usage more than doubled over a couple of days - even though I didn’t introduce any big features or libraries. So I started digging…
My first suspicion was that I have a memory leak somewhere . In Android we have the benefit of coroutine scopes that are scoped to a certain component - like viewModelScope
, lifecycleScope
, etc. These scopes do the clean up magic behind the scenes. If the component is destroyed - so is everything inside that scope. A graceful, neat solution.
Well, in a pure Kotlin app you have to manage that yourself. In my bot I would start up CoroutineScopes
for each operation and didn’t think twice of it. My thinking was - well, as long as everything finishes inside that scope - the memory will be released. And that is not wrong, but you have to be very careful with what’s going on inside the lambda.
Each scope created like this (CoroutineScope(Dispatchers.IO).launch { ... }
) lives independently — if you forget to cancel it or it captures large references, they’ll stay in memory long after you expect them to be gone
To be honest, I didn’t find the exact leak — but I decided to optimize my app anyway.
Here’s what helped cut memory usage in half 👇
- Made my data layer a singleton with Koin. Singletons help prevent redundant instantiations, but you also need to ensure they don’t themselves hold onto stale data forever.
- I switched some
StateFlow
s toSharedFlow
s where I didn’t need to retain the last emitted value —StateFlow
always holds one, which can keep large objects in memory unnecessarily. - Created my own application-wide
CoroutineScope
withSupervisorJob
instead of having dozens of independentCoroutineScope
s running wild. - Removed unnecessary logging in production
After these changes, memory dropped (see the graph), and my Railway bill went down too.


Moral of the story:
Don’t be reckless with memory. Sloppiness can cost you real money — especially outside mobile apps.
Member discussion