# Exit Code 137: OOMKilled or Just SIGKILL? How to Tell

> Exit code 137 means SIGKILL, and memory is only one cause. How to tell an OOM kill from a missed SIGTERM on deploy, and how to fix each one.

1. [Home](/)
2. [Blog](/blog)
3. Exit Code 137: SIGKILL, Not Always Out of Memory

DeploymentOctober 5, 202610 min read

# Exit Code 137: SIGKILL, Not Always Out of Memory

Exit code 137 says a process was killed with SIGKILL. It doesn't say who sent it. How to tell a memory kill from a missed SIGTERM on deploy or a failing probe, and what fixes each one.

## The short version

Exit code 137 is 128 plus 9: the process was killed with SIGKILL, which it cannot catch. The kernel sends it when a container hits its memory limit, but a runtime also sends it when a process ignores SIGTERM for too long, and probes or CI runners send it too. Check the OOMKilled flag first, then fix memory or shutdown handling.

[Teo Marquardt](/about#author)· Facts last checked October 2, 2026

The container is gone. The application log stops mid-line, with no stack trace and no error, and the only trace left is a status in `docker ps -a`: `Exited (137)`. Most answers you'll find say the same thing, that the container ran out of memory and needs a bigger limit. Often that's right. When it isn't, raising the memory changes nothing, and the container keeps dying with the same code.

The number tells you less than people assume. This article goes through the three different things that produce a 137, how to tell them apart from the container's state in under a minute, and the fix for each.

## What exit code 137 means

When a process is ended by a signal, the shell and container runtimes report its exit status as 128 plus the signal number. [Bash documents the rule](https://www.gnu.org/software/bash/manual/html%5Fnode/Exit-Status.html) and Docker and Kubernetes follow it. Signal 9 is SIGKILL, so 128 + 9 = 137\. A process can't catch, block or ignore SIGKILL, and the kernel removes it on the spot. Nothing in your code runs after that, not a `finally` block and not the last buffered log line, which is why the log just stops.

| Exit code | Signal       | What it usually means                                                                       |
| --------- | ------------ | ------------------------------------------------------------------------------------------- |
| 137       | SIGKILL (9)  | Killed outright. Memory limit, an expired grace period, a probe, or someone running kill -9 |
| 143       | SIGTERM (15) | Asked to stop and stopped. Normal during deploys and scale-downs                            |
| 139       | SIGSEGV (11) | Segmentation fault, usually in native code or a broken binary                               |
| 1         | none         | The application exited with an error by itself. Read its log                                |
| 0         | none         | Clean exit, including a SIGTERM handler that finished and called exit                       |

In a container, three senders cover nearly every case: the kernel enforcing a memory limit, the runtime giving up on a process that didn't stop when asked, and some other component that decided the container had to go.

## Exit code 137 but OOMKilled is false: read the container state first

The same code shows up as docker exit code 137 on a plain host and as kubernetes exit code 137 in a pod's last state, and the first check is the same. Before changing anything, ask the runtime what it recorded. Docker keeps an `OOMKilled` flag next to the exit code. Example output:

bash

```
$ docker inspect --format '{{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} finished={{.State.FinishedAt}}' api
137 OOMKilled=false finished=2026-10-01T14:02:41.118Z
```

Kubernetes shows the same information as the reason on the container's last state, which is where `Reason: OOMKilled` comes from. Example output:

text

```
$ kubectl describe pod api-7c9f8d6b5-x2kqp
...
    State:          Running
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
    Restart Count:  4
```

`Reason: OOMKilled` means the memory limit did it. `Reason: Error` with `Exit Code: 137` means something else sent the SIGKILL. That's the Kubernetes version of the "137 but OOMKilled is false" threads on the Docker forums. If you have access to the host, the kernel log settles it. Example output:

text

```
$ dmesg -T | grep -i 'killed process'
[Thu Oct  1 14:02:41 2026] Memory cgroup out of memory: Killed process 4127 (node) ...
```

`Memory cgroup out of memory` means the container hit its own limit. A line starting with just `Out of memory: Killed process` means the whole machine ran out, and the kernel picked a victim, possibly yours. The exact wording varies a little between kernel versions, so search for "Killed process" rather than the full line. `journalctl -k` shows the same log on systemd hosts.

| What you see                                                    | Who sent the SIGKILL                           | Where to look next                      |
| --------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------- |
| OOMKilled: true or Reason: OOMKilled                            | The kernel, at the container's memory limit    | Memory graph, runtime heap settings     |
| OOMKilled: false, died during a deploy, restart or scale-down   | The runtime, after the grace period ran out    | SIGTERM handling and what runs as PID 1 |
| OOMKilled: false, Out of memory: Killed process in the host log | The kernel, short of memory on the whole host  | Host memory and the neighbours on it    |
| Reason: Error, failed liveness probe in the pod events          | The kubelet, restarting an unhealthy container | Probe timing and slow startup           |
| Killed at the same point in every CI run                        | The runner, on a memory or time limit          | The job's resource limits               |

One caveat on the flag. In some setups `OOMKilled` stays `false` after a real memory kill, for example when the host ran out of memory rather than the container. Treat the flag as strong evidence when it's `true` and weak evidence when it's `false`. If the timing doesn't match a deploy and nothing else fits, check the kernel log anyway.

## Cause 1: the memory limit (OOMKilled: true)

A container's memory limit is enforced by the kernel through cgroups. When the processes inside it try to use more, the kernel's OOM killer picks a process in that group and sends it SIGKILL. There's no warning signal first, and nothing for your code to catch. If your main process is the one picked, the container exits with 137 and the runtime records `OOMKilled`.

### Your runtime doesn't know about the container limit

The most common cause isn't a leak. It's a runtime that sizes its heap without regard to the limit it's running under, and then grows into it.

* **Node.js.** V8 picks a maximum heap size on its own, and depending on the version that number may be based on more memory than your container has. Set it explicitly with `--max-old-space-size` (in megabytes), at roughly three quarters of the container limit so buffers and native memory have room. On a 512 MB plan that means something like `node --max-old-space-size=384 server.js`.
* **Java.** JVMs have read container limits since JDK 10, and 8u191 on Java 8, but the default heap is only a quarter of that limit (more on very small containers). Tune it with `-XX:MaxRAMPercentage` instead of a fixed `-Xmx`, so the setting follows the plan size if you change it.
* **Python and Ruby.** Neither has a heap ceiling at all. Memory grows until the limit is hit. Worker count matters more than anything: four Gunicorn or Puma workers each loading the full app use four times the memory of one.

### 137 during the build, not at runtime

If the 137 shows up in the build log, the application never started. `pip install` compiling a package with native extensions, `next build`, a webpack production build or `yarn install` on a large monorepo can all need more memory than the running app ever will. The fix there is the build's memory, or a lighter build step (prebuilt wheels, a narrower bundle), not the runtime plan.

### Raise the limit or find the leak

The memory graph decides this. A sawtooth that rises and drops again is a garbage collector doing its job; if the peaks touch the limit, the limit is too small for the work, and a bigger plan is the honest fix. A staircase that climbs steadily over hours and never comes back down is a leak, and a bigger limit only moves the 137 a few hours later. A flat line at the limit followed by a drop to zero is the kill itself.

## Cause 2: SIGTERM was ignored, so SIGKILL followed

This is the 137 that no amount of memory fixes, and the pattern gives it away: it happens on deploys and restarts and never in between. Stopping a container is a two-step process. The runtime sends SIGTERM and waits. If the process is still running when the grace period ends, it sends SIGKILL. [docker stop waits 10 seconds by default](https://docs.docker.com/reference/cli/docker/container/stop/), Compose uses the same default through `stop_grace_period`, and [Kubernetes waits 30 seconds](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination) through `terminationGracePeriodSeconds`.

A process that exits on SIGTERM never reaches the second step. A process that doesn't hear SIGTERM, or hears it and keeps going, sits there until the grace period ends and then goes out with 137\. During a [rolling or blue-green release](/blog/blue-green-vs-rolling-deployments) that happens to every old container being drained, which is why the code turns up in every deploy log.

SIGKILL can't be handled

Every fix for a 137 lives before the SIGKILL. Code can react to SIGTERM; nothing reacts to SIGKILL. A shutdown routine that needs 40 seconds under a 30-second grace period will be cut off every single time.

### The PID 1 problem

The first process in a container runs as PID 1, and Linux treats PID 1 differently: it gets no default signal actions, so a signal it has no handler for is simply ignored. A Node or Python app that never registered a SIGTERM handler would normally die on SIGTERM; as PID 1 it shrugs the signal off and waits for the SIGKILL.

The Dockerfile often makes this worse. The shell form of `CMD` runs your command through `/bin/sh -c`, so depending on the shell, PID 1 may be `sh`, with your app as a child that never gets the signal. `npm start` adds another layer of its own between the signal and your code.

dockerfile

```
# Shell form: runs under /bin/sh -c, and npm adds another layer.
# SIGTERM may never reach node.
CMD npm start

# Exec form: node is PID 1 and gets SIGTERM directly.
# It still needs a handler (below) or --init to act on it.
CMD ["node", "server.js"]
```

If you need a shell script as the entrypoint, end it with `exec node server.js` so the app replaces the shell instead of running under it. Another option is a minimal init process: [docker run --init](https://docs.docker.com/reference/cli/docker/container/run/#init), or `init: true` in Compose, puts `tini` in as PID 1, and tini forwards signals to your app and reaps zombie processes. Your app is then no longer PID 1, so a SIGTERM it has no handler for ends it the normal way.

### Handle SIGTERM and finish inside the grace period

Receiving the signal is half of it. The app then has to stop taking new work, let what's in flight finish, close its connections and exit, all before the grace period runs out.

javascript

```
const server = app.listen(process.env.PORT);
// db is your connection pool

process.on("SIGTERM", () => {
  console.log("SIGTERM received, draining");
  server.close(async () => {
    await db.end();
    process.exit(0);
  });
  // Node 18.2+: drop idle keep-alive sockets so close() can finish
  server.closeIdleConnections?.();
  // Leave on our own terms if draining takes too long.
  setTimeout(() => process.exit(1), 8000).unref();
});
```

The timeout has to sit below the grace period, with a margin. Background work needs the same treatment: a worker in the middle of a job should either finish it or put it back on the queue. A scheduled task that starts five seconds before a deploy is the classic victim, which is one more reason [where your cron schedule lives](/blog/cron-job-multiple-instances) matters when several instances come and go.

### Exit code 143 or 0 means shutdown works

Once SIGTERM reaches the app, the deploy log changes. A process that isn't PID 1, for example one running under `--init`, and has no handler dies from SIGTERM and exits with 143\. One whose handler drains and calls `process.exit(0)` exits with 0\. Both mean the stop happened inside the grace period. Seeing `exit code 143` after a deploy is not an error to chase; 137 is the one that means requests were cut off.

## Cause 3: something else sent the SIGKILL

* A failing liveness probe. When a Kubernetes liveness probe fails often enough, the kubelet restarts the container through the same stop sequence, so an app that ignores SIGTERM goes out with 137 and `Reason: Error`. Slow startups are the usual culprit: the probe starts checking before the app is ready. A startup probe or a longer initial delay fixes it.
* The host itself running out of memory. If the machine runs out before your container reaches its own limit, the kernel kills whatever scores worst, and that can be a container that was well within its limit.
* A person or a script: `docker kill`, `kill -9` or a cleanup job sends SIGKILL directly, with no SIGTERM first.
* A CI runner. GitLab runners, GitHub Actions and Jenkins agents kill jobs that exceed their memory or time limits, and the job reports 137\. If it fails at the same step every time, the step needs more memory than the runner has.

### command terminated with exit code 137

This exact message comes from `kubectl exec` when the command you ran inside a pod was killed. Often the pod was already close to its memory limit, and your extra process (a database dump, a migration, a console session) pushed it over. The OOM killer picks the highest-scoring process in the container, which may be your command or the app itself, so check `kubectl describe pod` for a restart before blaming the command.

One that looks related but isn't: if your Redis log shows `OOM command not allowed`, Redis is refusing writes on purpose under its `maxmemory` setting, and nothing was killed. That's a [different error with a different fix](/blog/redis-oom-command-not-allowed). A Redis container that exits with 137 is the opposite case, usually with `maxmemory` left at 0, so the kernel enforced the limit instead.

## How Runsite handles it

On Runsite every [web service runs on a plan with a fixed memory allowance](/services/web-services), from 256 MB to 16 GB, and moving to a bigger plan is a setting, not a code change. Memory and CPU graphs sit in the dashboard next to the real-time logs, so you can tell a sawtooth from a staircase before the container dies. Containers that crash are restarted automatically, and health checks catch an app that is running but not answering.

Deploys are rolling: new containers start next to the old ones, traffic moves once the health checks pass, and the old containers are then stopped. As on any platform, there's a window between the stop request and a forced kill, so the rules above apply unchanged: exec-form `CMD`, a SIGTERM handler, and a shutdown that finishes in seconds. Get those right and the old version drains instead of being cut off mid-request. The services run in Germany, and their logs stay in the EU too.

## The short version

* Exit code 137 is 128 + 9: the process got SIGKILL, which it cannot catch. It tells you how the process died, not who killed it.
* Check `docker inspect` for `OOMKilled` or `kubectl describe pod` for `Reason: OOMKilled` before you change anything.
* If it was memory, set the runtime's heap below the container limit, then use the memory graph to tell a too-small limit from a leak.
* If it happens on every deploy, the app isn't stopping on SIGTERM. Use exec-form `CMD` or `--init`, handle SIGTERM, and finish inside the grace period.
* Exit code 143 or 0 on deploy means shutdown works. 137 means it ran out of time.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](/services/web-services)

[Back to all articles](/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does exit code 137 mean?

Exit code 137 means the process was killed by signal 9, SIGKILL, because runtimes report a signal death as 128 plus the signal number. SIGKILL cannot be caught or ignored, so the process stops immediately without running any cleanup code. The most common sender is the Linux kernel when a container exceeds its memory limit, but a container runtime also sends SIGKILL when a process does not exit within the grace period after SIGTERM, and probes, CI runners or a manual kill -9 can send it too.

### What causes a container to exit with code 137?

Three things in practice. The container hit its memory limit and the kernel's OOM killer ended the main process, which Docker records as OOMKilled true and Kubernetes as Reason: OOMKilled. Or the container was being stopped, for a deploy, restart or scale-down, and the process ignored SIGTERM until the grace period ran out, usually because it ran as PID 1 without a SIGTERM handler or behind a shell or npm that didn't pass the signal on. Or another component killed it directly: a failing liveness probe, the host running out of memory, a CI runner limit, or docker kill.

### What does OOMKilled mean?

OOMKilled means the Linux out-of-memory killer terminated the container's process because the container tried to use more memory than its limit allowed. Docker shows it as State.OOMKilled set to true in docker inspect, and Kubernetes shows Reason: OOMKilled with exit code 137 in kubectl describe pod. The fix is either a larger memory limit, if the workload genuinely needs it, or a smaller footprint: a heap size set below the limit, fewer worker processes, or a fixed memory leak.

### How do I fix exit code 137?

Start by finding out which kind it was. If OOMKilled is true, set the runtime's heap below the container limit, for example with --max-old-space-size in Node or -XX:MaxRAMPercentage in Java, check the memory graph for a leak, and raise the limit if the workload needs it. In Kubernetes that means raising resources.limits.memory. If OOMKilled is false and the 137 happens on deploys or restarts, make the app stop on SIGTERM: use exec-form CMD, an init process such as tini, and a shutdown handler that finishes inside the grace period.

Keep reading

## Related articles

[Deployment11 min readBlue-Green vs Rolling Deployments: What Zero Downtime Costs YouBoth strategies promise your users never see the release. The difference that decides between them is how many versions of your code are talking to one database at the same time.Aug 17, 2026Read](/blog/blue-green-vs-rolling-deployments)[Caching12 min readOOM command not allowed: Redis maxmemory and Eviction PoliciesRedis is refusing writes and still serving reads. The error is not a crash, it is a policy doing exactly what you configured, and two defaults nobody chose put you there.Sep 2, 2026Read](/blog/redis-oom-command-not-allowed)[Scheduling14 min readCron Jobs on Multiple Instances: Why Your Scheduled Task Runs TwiceA scheduler inside your app is a timer in memory, and every replica has its own. What that does under autoscaling, rolling deploys and sleeping instances, and where the schedule should live instead.Sep 11, 2026Read](/blog/cron-job-multiple-instances)

## Your app deserves to be online

€5 of credit on signup. Deploy in under a minute. No credit card needed.

[Start deploying](https://dashboard.runsite.app/login)[View documentation](https://docs.runsite.app)

---

Source: https://runsite.app/blog/exit-code-137
