App keeps restarting: out-of-memory (OOM) errors explained

An out-of-memory (OOM) error means your app asked for more RAM than its limit allows, so the runtime or the kernel stopped it. Read the log, find what grows, then fix the leak or raise the limit.

Updated 7 min read

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
On this page

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.

RAMtimememory limitflat usageleakOOM killrestart
Flat usage never drops. A leak climbs to the limit, the process is killed and the cycle starts over on every restart.

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:

text
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

The 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:

  1. 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.
  2. Loading too much at once. Reading a whole large file or a whole database table into memory instead of streaming or paging it.
  3. 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.
  4. 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.
  5. 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:

js
// 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:

bash
node --max-old-space-size=400 --heapsnapshot-near-heap-limit=2 index.js

Open 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:

python
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:

bash
java -Xmx384m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dumps/ -jar app.jar

Open 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-size to 1536 to leave room for other uses. Apply the same proportion to your limit. For Java, set -Xmx explicitly, 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

Learn more

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.

Next articleWhat is Docker?Docker is a platform that packages an application and its dependencies into an image and runs it as a container. See how it works, a working example and its limits.

More in Deploy

See all 33 articles