← Back to Wiki
Debugging / Linux

Perhaps Out Of Memory Is A Guess, Not A Measurement

A service logs an out of memory error. You give it more memory. The error was never about memory, and now you have spent RAM you did not have on a problem you did not fix.

Share on X

I did this one to myself. Here is the whole thing.

The message

[ERROR] Worker (pid:14630) was sent SIGKILL! Perhaps out of memory?

Seven of those in a day. On a container with 1GB of RAM.

That looks like a finished sentence. Small container, out of memory error, seven times a day. So I doubled it to 2GB and rebooted.

Then I checked, and none of it held up.

Read the line above it

Every one of those SIGKILL lines had a partner:

[CRITICAL] WORKER TIMEOUT (pid:14630)
[ERROR] Worker (pid:14630) was sent SIGKILL! Perhaps out of memory?

The timeout came first. gunicorn killed the worker because it stopped answering, not because it ran out of memory.

gunicorn prints Perhaps out of memory? for any worker it has to SIGKILL. It does not check. It is a hint in the message text, written by a developer guessing at the most common cause.

I read a guess as a diagnosis.

Ask the kernel instead

The kernel is the only thing that knows if it killed something for memory.

Check with command journalctl -k --since '7 days ago' | grep -iE 'oom|killed process'

Zero results. Seven days.

An actual out of memory kill always leaves a kernel record. It prints the process it chose, the score, and a table of what was using memory. If that is not there, the kernel did not do it.

No kernel OOM line means no OOM. Full stop.

Then measure the thing you blamed

I assumed the 15 minute sync job was the memory hog.

Run it by hand and watch:

free -m | sed -n 2p
time docker exec container /srv/app cron run_jobs
free -m | sed -n 2p

12 seconds. Memory went from 383MB to 385MB.

2MB. On a 2048MB container.

The job I suspected was not using memory at all.

The part that actually embarrassed me

I had claimed the dead workers explained why my inventory sync kept under delivering hosts.

The sync does not run through gunicorn. It runs as its own process out of the container's crond.

A dead web worker cannot touch it. The two things were never connected. I linked them because they were both in the same container and both looked broken.

Check that the component you are blaming is even in the path of the thing that is failing.

What it really was

gunicorn defaults to a 30 second timeout. Two workers, no timeout set in the config.

Almost no HTTP requests were being logged at all, so these were not slow requests. They were workers that missed a heartbeat.

The host is a Proxmox box running 42 guests. Under load a worker stalls past 30 seconds, the master kills it, and a replacement boots 45 seconds later.

The container was fine. The hypervisor was busy.

BE WARNED: load average inside an LXC is not yours

This one has caught me twice now.

Run uptime inside an LXC container and you get a load average around 30. That looks like your container is on fire.

It is not. On most kernels /proc/loadavg is not namespaced, so every container reports the host's load as its own.

Run uptime on the hypervisor and compare. If the numbers match, you are reading the host's load, and it tells you nothing about the container.

Same trap applies to PageTables in /proc/meminfo. If a metric looks identical across containers doing completely different jobs, it is leaking from the host.

What I did in the end

Nothing.

The impact is a web admin page that blips a few times a day. The sync was never affected.

Raising the gunicorn timeout would hide host contention rather than fix it, and the config lives inside the container image, so an update would wipe it.

I left the extra RAM in place as headroom and wrote down that the diagnosis was wrong.

Not every alert needs a change. Some need an accurate note.

The general lesson

Error messages contain two different things. Facts about what happened, and the author's guess at why.

WORKER TIMEOUT is a fact. Perhaps out of memory? is a guess, and it even has a question mark on it.

Before you act on the why, go find a source that measured it. The kernel log, real usage numbers, a timing run.

Adding resources makes almost any symptom quieter for a while. That is what makes it such an easy wrong answer.