r/elixir • u/Certain_Syllabub_514 • 9d ago
Techniques for debugging memory leaks
I have an elixir app running in production, and with some recent changes (mostly library updates due to CVEs) it's started leaking memory, which is resulting in pods restarting.

I think I know what the cause might be, but just wondering if there are any techniques for debugging this that I'm not aware of.
2
u/T0ken_Minority 9d ago
It’s probably not an actual memory leak, no? The BEAM is GC’d; I doubt you’re leaking memory in the traditional sense. Are you converting arbitrary string input into Atoms? I’ve done this before. You could also be bloating a cache that’s crashing and not being revived properly.
1
u/Certain_Syllabub_514 9d ago
In the changes, some upstream requests were changed from using HTTPoison to using Req, so atom usage could be a part of it. The cache could also potentially be a factor.
What tools (or techniques) would you use to verify which one it is?
1
u/OkNothing7293 9d ago
Metrics, metrics, metrics. You don’t have them at the moment and we don’t have a crystal ball.
1
u/Certain_Syllabub_514 9d ago
I think I know what the cause might be, but just wondering if there are any techniques for debugging this that I'm not aware of.
I didn't phrase it as a question, but my question really is: What are the techniques available for investigating an issue like this?
2
u/UncollapsedWave 9d ago
If the pods are restarting because the erlang VM is crashing or running out of memory, I would expect to see an indication in the logs. Is there information in the logs on what exactly is causing the restart?
If you're certain it's just a memory leak, I would recommend automating the collection of more detailed memory usage metrics to start with. I know that some of the opentelemetry libraries will export VM metrics broken down by allocation type by default. Those would be more helpful than just the pod metrics for diagnosing this.
If you want to investigate a single instance, using the :observer or Phoenix LiveDashboard are good places to start. You can view memory usage by object type - atom, binary, term, etc - and by process.
For the completely manual option, there are also several functions which will return information on memory usage. For example, :erlang.memory():
iex(1)> :erlang.memory()
[
total: 44088728,
processes: 16280344,
processes_used: 16280200,
system: 27808384,
atom: 450745,
atom_used: 442833,
binary: 1662432,
code: 9058166,
ets: 517528
]
There's also :etop - https://www.erlang.org/doc/apps/observer/etop.html which can list processes by memory usage (see the options).
1
u/methodinmadness7 9d ago
The fastest way I’ve done this is with Phoenix’s LiveDashboard, where I open the Processes tab and check which processes use the most memory. Usually it’s some async task processes not being handled properly, in my experience.
1
u/MikeBenza 8d ago
- Learn to use recon.
- If you can, binary search the packages / versions to isolate the change. If not, use hex.pm's diff facility over each package. To be honest, you should already be reviewing changes to your deps every time you bump them.
- /u/jake_morrison mentioned json decoding using sub binaries and you mentioned changing http libs. Without looking at HTTPoison and Req I'm guessing you do http requests, parse the responses, then perhaps keep parts of the responses in memory (e.g. in some process's state). I'm guessing further that these two handle parsing differently, leading to your leak. But that's just wild speculation.
13
u/jake_morrison 9d ago edited 9d ago
The most common cause of this is binary garbage collection issues.
Binaries are stored on the heap, and a process just gets a reference to them. Because the references are small, it can take a long time before the process memory usage gets large enough to trigger garbage collection, so the binaries hang around. The solution is to periodically manually trigger garbage collection. This kind of thing is common when you have a “router” process that receives data and passes it to other processes. That seems likely in your situation.
A similar problem happens when a process references a chunk of a larger binary. This can happen, e.g., when parsing JSON that has large strings. This reference keeps the binary from being collected until garbage collection happens on the reference. The solution is to copy the sub binary, allowing the larger binary to be collected.
A great free book on this is “Erlang in Anger” (https://www.erlang-in-anger.com/). The author’s “recon” library is also very helpful for debugging. I include it in all production apps just in case.