Learning objectives
After reading this article you will be able to:
- Tell a runtime OOM error apart from a silent kernel kill
- Find out what is eating memory in Node.js, Python or Java
- Fix the cause and set limits so it does not come back
Your app runs fine for an hour, a day or a week, then it disappears and starts again. The code did not change and there is no stack trace. This pattern, a restart with no code error, is the classic fingerprint of an out-of-memory crash.
Memory is the one resource that does not degrade gracefully. A CPU shortage makes the app slower. A memory shortage ends the process.
What is an out-of-memory error?#
Every process needs RAM for its code, its data and the runtime that runs it. A container has a memory limit. When the app tries to use more than that limit and the system cannot free anything, something has to give.
The Linux kernel documents it for cgroups: if a cgroup's memory usage reaches memory.max and can't be reduced, the OOM killer is invoked in that cgroup. Docker says the same in plain terms: by default, if an OOM error occurs, the kernel kills processes in a container.
In practice, people use "OOM" for two different failures:
- Runtime OOM. The language runtime notices it is out of heap and aborts with a message you can read in the log.
- Kernel OOM kill. The kernel ends the process from outside. The app cannot catch it and writes no message.
How do I know it is an OOM crash?#
Check these signs, in order.
1. The log message. A Node.js runtime OOM looks like this:
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryThe middle of the message varies (you may also see Ineffective mark-compacts near heap limit), but it ends with JavaScript heap out of memory.
Python raises MemoryError and Java throws java.lang.OutOfMemoryError (for example Java heap space). If you see one of these, you have a runtime OOM and the stack trace tells you where it happened.
2. A log that just stops. For a kernel kill, the last line is ordinary output and the next one is the startup banner, with no error in between. Combined with the next sign, this is nearly conclusive.
3. Exit code 137. If your tooling shows exit codes, 137 means 128 plus signal 9 (SIGKILL) by shell convention. It is what you see when the kernel kills a container process.
4. The memory graph. Look at RAM over time. A line that rises and then drops to near zero at each restart means the app hit the limit. If you are not sure where to find that output, see how to read your app logs.
If the graph is flat and low when the crash happens, it is probably not memory. Look for an uncaught exception instead.
What causes an app to run out of memory?#
From most to least common:
- A memory leak. Something keeps references that it should drop: a growing array, a cache with no size limit, an event listener added on every request, a map keyed by user ID that is never cleaned.
- Loading too much at once. Reading a whole large file or a whole database table into memory instead of streaming or paging it.
- A limit that is simply too small. The app is healthy but its normal peak is above what it was given. See how much RAM and CPU your app needs.
- Runtime limit below container limit. Node.js and the JVM have their own heap caps, separate from the container. Memory outside the heap (buffers, native libraries, thread stacks) counts against the container but not against the heap, so the process can be killed while the heap looks fine.
- Traffic spikes. Many concurrent requests each holding a large object. Bots in many servers hit this too (see how much RAM a Discord bot needs).
How do I find what is using the memory?#
Measure before you guess. The goal is to see which objects grow.
Node.js#
Log memory every few seconds. rss is the whole process and is what counts against the container:
// memory-log.js: load it first in your entry file (require or import)
const mb = (n) => Math.round(n / 1024 / 1024);
setInterval(() => {
const m = process.memoryUsage();
console.log(`rss=${mb(m.rss)}MB heapUsed=${mb(m.heapUsed)}MB external=${mb(m.external)}MB`);
}, 30_000).unref();If heapUsed climbs with rss, it is a JavaScript leak. If rss climbs but heapUsed stays flat, look at buffers, native modules or large streams.
To get a heap snapshot just before the crash, start Node with --heapsnapshot-near-heap-limit. The Node.js docs describe it as writing a V8 heap snapshot to disk when heap usage approaches the limit:
node --max-old-space-size=400 --heapsnapshot-near-heap-limit=2 index.jsOpen the resulting .heapsnapshot file in Chrome DevTools (Memory tab) and sort by retained size. Taking snapshots costs time and memory itself, so use this to investigate, not as a permanent setting.
Python#
The standard library has tracemalloc. Start it early, take a snapshot later and print the lines that allocated the most:
import tracemalloc
tracemalloc.start()
leaky = [bytes(1000) for _ in range(50_000)] # stand-in for your real workload
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics("lineno")[:10]:
print(stat)Tracing adds overhead, and a snapshot only covers allocations made after start(), so turn it on while debugging.
Java#
Ask the JVM to write a heap dump when it throws OutOfMemoryError. By default this is disabled:
java -Xmx384m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dumps/ -jar app.jarOpen the .hprof file in a tool such as Eclipse MAT or VisualVM and look at the largest retained objects.
How do I fix an OOM crash?#
Match the fix to the cause.
- Leak: remove the reference. Bound every cache (a maximum number of entries and a time to live), remove event listeners you add, and stop storing per-user data forever.
- Too much data at once: stream files, paginate queries and process in batches instead of building one giant list.
- Limit too small: raise the app's memory to the measured peak plus 30% to 50% headroom, as explained in the sizing article above.
- Runtime cap mismatch: set the heap cap below the container limit so the runtime fails with a readable error instead of a silent kill. On Node.js the docs suggest, for a 2 GiB machine, setting
--max-old-space-sizeto 1536 to leave room for other uses. Apply the same proportion to your limit. For Java, set-Xmxexplicitly, since the default is chosen at runtime from system configuration. - Spikes: limit concurrency, add a queue, or move heavy work (image processing, large exports) to a separate process.
Raising the limit is a valid fix only when usage is stable. If the graph climbs forever, a bigger limit just makes the crash later and harder to reproduce.
How do I prevent it from coming back?#
- Record memory over time, as in the snippets above, and look at it after each release.
- Set explicit heap caps so failures are loud.
- Load test with realistic data before launch, not an empty database.
- Alert yourself when memory stays above about 80% of the limit for a long time, using an external uptime or metrics monitor.
- Restart on a schedule only as a stopgap while you fix a known leak, never as the fix.
How to do this on Square Cloud#
On Square Cloud, each application runs in an isolated container with its own memory allocation, and the plan's RAM is a pool you split between your applications. The minimum is 256 MB for a background application such as a bot and 512 MB for a web application, and a single application can use up to 32 GB.
When an application runs out of memory, it stops and stays stopped: automatic restart (on by default for dashboard deploys) only covers a process that exits with an error, so you raise the RAM and start it again. The real-time logs in the dashboard, CLI and VS Code extension keep the output from before the stop, so you can read the last lines before the crash. Metrics for CPU, RAM and network are sampled every 5 minutes and kept for 24 hours, which can show the climb-and-drop shape of a leak that crashes within a day.
To fix a limit that is too small, change the application's RAM in its settings or through the API. This is vertical scaling, done by you: there is no autoscaling. The rest of the logging and metrics features are on the features page.
Deploy
Push from GitHub, the CLI or a ZIP and go live in seconds
FAQs
What does exit code 137 mean?
By shell convention, 137 is 128 plus 9, meaning the process was ended by SIGKILL. In a container this is the usual sign of an OOM kill, but any other forced kill produces the same code, so confirm with the memory graph.
Why is there no error in my logs when the app dies?
When the kernel kills a process for exceeding its memory limit, the process gets no chance to log anything. You only see the log simply stop, followed by a restart.
Is a memory leak the same as an OOM error?
No. A leak is memory that grows and is never released. An OOM error is the result when usage reaches the limit. A leak is one cause, but an app can also hit the limit by loading too much data at once.
Should I just buy more RAM?
If usage is flat and just above the limit, more RAM is the right fix. If usage climbs steadily until it dies, more RAM only delays the crash and you need to fix the leak.
More in Deploy
See all 33 articles- What are environment variables?Environment variables are key-value pairs the operating system hands to a process at startup. Learn to read them in Node.js and Python and keep tokens out of code.
- What is a deployment?A deployment takes a version of your code to a server, prepares it and starts it. See the stages, the methods and how to roll back when it goes wrong.
- How much RAM and CPU does my app need?Size RAM and CPU by measuring your app's real peak and adding about 30% headroom. Learn how to measure in Node.js, Python and Java and what to do if short.
- Automatic deploys with GitHub ActionsAutomatic deploys with GitHub Actions ship your code to the server on every push. See the workflow step by step, secrets, tests before deploy and common errors.