Diagnosing OOM Kills in Docker: Reading the Silent Verdict the Kernel Leaves Behind

Diagnosing OOM Kills in Docker: Reading the Silent Verdict the Kernel Leaves Behind

A ground-up guide to recognizing, investigating, and understanding why the Linux kernel suddenly terminated a container process — and how to tell the difference between a genuine memory problem and something else wearing its costume.

Imagine coming back to your desk to find a colleague’s chair empty, their computer still logged in, but no note, no goodbye, nothing in their calendar explaining where they went. Something clearly happened, and happened abruptly — but the evidence isn’t sitting on the desk itself; it’s scattered across the building’s own security logs, badge records, and the building manager’s own private notes. A container killed by the Linux kernel’s out-of-memory killer looks exactly like that empty chair: the application logs often show nothing useful at all, because the process was terminated too abruptly to log its own death. The real evidence lives one level up, in the kernel and in Docker’s own bookkeeping — and this tutorial is a guide to finding it, reading it correctly, and telling a genuine OOM kill apart from every other reason a container might quietly disappear. By the end, the goal is to be able to look at any unexplained container exit and know, within a few minutes, exactly which evidence source to check first.

1What an OOM Kill Actually Is

Before hunting for evidence, it’s worth being precise about what event we’re actually looking for.

The one-sentence definition

An OOM kill (out-of-memory kill) happens when the Linux kernel forcibly terminates a process because the memory boundary it’s operating within — for a container, its cgroup memory limit — has been exhausted, with no more memory left to reclaim or swap out. It is not a crash the application chose, requested, or was even necessarily aware was coming; it’s an external, kernel-level decision made entirely outside the application’s own control flow.

Simple Analogy

Picture a shared filing room with a strict rule: no more than ten boxes allowed inside at once. When someone tries to bring in an eleventh box and there’s genuinely nowhere left to put it, the building manager doesn’t ask permission — they simply grab whichever box seems least essential and haul it out the door immediately, mid-task, so the room’s limit is respected. The OOM killer is that building manager, and the process it removes rarely gets any warning first.

The Actor

OOM Killer

A Linux kernel subsystem that selects and terminates a process when memory is critically exhausted within a given scope.

The Boundary

cgroup memory limit

The per-container memory ceiling Docker configures, which defines the scope the kernel is enforcing when a container-level OOM kill happens.

The Signal

Exit code 137

The specific process exit code a container typically shows after being killed this way, covered in detail later in this tutorial.

The Record

Kernel log entry

A detailed message the kernel writes explaining exactly what it killed and why, the single most authoritative piece of evidence this whole tutorial revolves around.

i
Why “diagnosing” is its own skill

An OOM kill often looks, from the outside, identical to a dozen other failure modes — a crash, a health-check failure, an orchestrator eviction. Diagnosing it correctly means learning to read the specific evidence only the kernel and Docker leave behind, rather than guessing from symptoms alone.

Why this is a genuinely different debugging skill from application-level debugging

Most debugging a developer does day to day involves reasoning about code they can read, running in a process they can attach a debugger to, producing logs they control the content of. An OOM kill breaks all three of those assumptions at once: the decision is made entirely outside the application’s own code, the process is gone before any debugger could meaningfully attach, and the application never got a chance to write anything about its own final moments. This is precisely why treating an OOM kill investigation like an ordinary application bug — reading stack traces, adding print statements, stepping through code — tends to go nowhere, and why this tutorial instead focuses on the separate, kernel- and platform-level evidence trail covered in the chapters ahead.

2Core Concepts: Memory Limits, Cgroups, and the Killer

Three pieces work together to make an OOM kill possible in the first place. Understanding each one separately makes the combined event much easier to reason about.

The limit: a promise the container can never break

When a container is started with a memory limit, Docker writes that value into the container’s cgroup — a kernel-managed accounting and enforcement boundary. Unlike a soft warning or a monitoring alert, this limit is a hard ceiling the kernel itself actively enforces: the cgroup’s total memory usage is simply never allowed to exceed it, no exceptions, no grace period.

The exhaustion: nowhere left to reclaim from

As covered in earlier tutorials on memory and storage internals, the kernel has several tools to free up memory under pressure before resorting to killing anything — reclaiming file-backed page cache, swapping out anonymous memory if swap is available. An OOM kill specifically happens once all of those options have been exhausted within the relevant scope, and a process is still requesting more memory than remains available.

The killer: a last-resort decision-maker

The OOM killer is the kernel component that steps in at exactly that point. Its job isn’t to prevent the system (or cgroup) from running out of memory — by the time it activates, that’s already happened — its job is to restore a workable state as quickly as possible by choosing one process to sacrifice, freeing its memory immediately.

Simple Analogy

Think of an elevator with a strict weight limit and a sensor that refuses to move once it’s exceeded. The limit itself doesn’t prevent people from trying to squeeze in; it just means that once the sensor trips, someone has to step back out immediately before the elevator can function again — and that decision happens fast, without a vote or a warning period.

137
Typical container exit code following an OOM kill
1
Process usually terminated per OOM event, in a typical single-process container
0
Warning the application code typically receives before termination

3Architecture: Where the Evidence Actually Lives

Diagnosing an OOM kill is fundamentally an exercise in knowing which of several separate systems recorded what — no single place tells the whole story on its own.

Evidence sourceWhat it actually showsWhy it matters
Kernel log (dmesg / journal)The full OOM killer report — memory state, candidate scoring, chosen victimThe single most authoritative, detailed source of truth
docker inspect container stateAn explicit OOMKilled boolean flag and the container’s exit codeThe fastest confirmation that this specific failure mode occurred
Docker events streamA timestamped record of the container’s death, alongside other lifecycle eventsUseful for correlating the kill against deploys, restarts, or other activity
Container’s own application logsUsually nothing at all, or output that simply stops mid-streamThe absence of a graceful shutdown log is itself a meaningful clue
cgroup memory statisticsHistorical memory usage leading up to the event, if captured by monitoringShows the trend that led to exhaustion, not just the final moment
graph TD
    A["Container memory exhausted"] --> B["Kernel OOM killer activates"]
    B --> C["Kernel log records full event"]
    B --> D["Process terminated with SIGKILL"]
    D --> E["Container exits with code 137"]
    E --> F["Docker records OOMKilled: true"]
    E --> G["Docker events stream logs death"]
    C -.->|"most detailed source"| H["Full diagnostic picture"]
    F -.->|"fast confirmation"| H
    G -.->|"timing context"| H
        
FIG 1 — How one OOM event fans out into several separate, complementary evidence sources
!
Common Mistake

Checking only the container’s own application logs and concluding, when nothing useful appears there, that the cause is a mystery. The application usually has no opportunity to log anything about its own termination — the evidence was never going to be there in the first place.

Why no single source is sufficient on its own

It’s worth being explicit about why this tutorial insists on checking multiple sources together rather than picking one “best” place to look. The kernel log is the most detailed, but it’s host-wide and requires correctly matching the right cgroup to the right container. The OOMKilled flag is the fastest and most unambiguous, but it tells you nothing about why the memory grew in the first place. The events stream gives timing context but no memory detail at all. Historical cgroup statistics show the trend but require monitoring to have already been capturing them before the event happened. A complete diagnosis typically draws on at least two or three of these sources together, each compensating for what the others lack.

4Internal Working: How the Kernel Picks a Victim

Understanding the selection logic behind an OOM kill turns the kernel log’s cryptic-looking numbers into something genuinely readable.

The badness score

When the OOM killer activates within a given scope, it evaluates every candidate process in that scope and assigns each one a numeric “badness” score, primarily driven by how much memory that process is currently using — a process consuming more memory generally scores as a “worse” candidate to keep alive, and thus a more likely candidate to be killed. The process with the highest resulting score is the one selected for termination.

oom_score_adj: a deliberate thumb on the scale

Every process also carries an adjustable value, oom_score_adj, that shifts its computed badness score up or down before the final comparison — a way for the system (or occasionally an administrator) to declare certain processes more or less expendable than raw memory usage alone would suggest. In container platforms, this value is sometimes set deliberately based on a container’s configured priority, so that, when multiple containers share a host and one has to be sacrificed, a lower-priority workload is more likely to be chosen over a critical one.

Simple Analogy

It’s like a triage list at a busy help desk, where each waiting request gets a base priority score from how large its request is, but staff can also manually flag certain requests as “handle last” or “protect at all costs” regardless of size — the final order reflects both the raw measurement and that deliberate override.

Scoped to the cgroup, not always the whole host

Critically, when a container has its own memory limit configured, an OOM event triggered by that container hitting its own ceiling is scoped to just that container’s cgroup — the kernel only considers processes within that boundary as candidates, even if the host machine as a whole has abundant free memory elsewhere. This cgroup-aware OOM behavior is what makes per-container memory limits meaningful in the first place: it contains the failure to the one workload that actually exceeded its own budget.

sequenceDiagram
    participant K as Kernel OOM killer
    participant C as Container's cgroup
    participant P1 as Process A (high memory)
    participant P2 as Process B (low memory)
    K->>C: Memory limit exceeded, no reclaim left
    K->>P1: Compute badness score
    K->>P2: Compute badness score
    Note over K: P1's score is higher (more memory used)
    K->>P1: Send SIGKILL
    P1-->>K: Terminated immediately
    Note over C: Memory freed, cgroup returns under its limit
        
FIG 2 — Victim selection scoped to a single container’s cgroup
i
Why this matters for multi-process containers

In a container running several processes, this scoring means the killed process isn’t necessarily “the container’s main process” by default — it’s whichever process within that container’s cgroup scored highest, which is worth knowing when a container with multiple processes survives an OOM event but behaves strangely afterward, having lost one specific internal process rather than being fully restarted.

Why memory usage, not memory allocation history, drives the score

It’s worth being precise that the badness score is driven primarily by a process’s current, resident memory usage at the moment of the OOM event, not by how much memory it has allocated and freed over its lifetime, nor by how quickly it grew. Two processes with identical current memory footprints score similarly regardless of whether one got there gradually over hours and the other spiked there in seconds — the kernel’s decision is a snapshot judgment, not a historical one. This is exactly why the historical trend, covered in a later chapter on cgroup statistics, has to come from separate monitoring rather than from the kernel’s own OOM report, which only ever captures the single moment of the kill itself.

5Data Flow & Lifecycle: From Pressure to Exit

Tracing the full sequence from rising memory usage to a container’s final exit state shows exactly where each piece of diagnostic evidence gets written.

1

Memory usage rises toward the limit

The container’s cgroup usage climbs, whether from a genuine workload spike, an unbounded cache, or a memory leak — nothing unusual is logged yet at this stage.

2

Kernel reclaim attempts

As the ceiling approaches, the kernel attempts ordinary memory reclaim — freeing file-backed pages, swapping anonymous pages if allowed — buying time without killing anything yet.

3

Reclaim options exhausted

Usage hits the hard limit with no further memory reclaimable within the cgroup’s scope — the point at which the OOM killer is invoked.

4

Victim selection and termination

The kernel scores candidate processes, selects one, and sends it an immediate, uncatchable termination signal — the process has no opportunity to run its own shutdown or cleanup logic.

5

Kernel log entry written

The kernel writes a detailed report of the event — memory state, the process killed, its score — to the kernel log, independent of anything Docker itself does next.

6

Container exit and Docker’s own bookkeeping

Docker observes the container’s main process has exited, records the OOMKilled flag and the exit code, and emits a lifecycle event — this is the layer most diagnostic workflows check first.

7

Restart policy evaluation

If the container has a restart policy configured, Docker may start it again immediately — which, without the memory pressure being addressed, can lead directly back to step 1, producing a restart loop.

!
Why step 4 matters so much for diagnosis

Because termination is immediate and uncatchable by ordinary application signal handling, there is no reliable window for the application to log a final message explaining what was happening at the moment of death. This is precisely why the kernel log, not the application log, is the primary diagnostic source for this specific failure mode.

6Recognizing an OOM Kill: Exit Code 137 and the OOMKilled Flag

Before digging into detailed kernel logs, there’s a fast, reliable first check worth running on any suspicious container exit.

Why 137, specifically

A container’s exit code following a signal-based termination is conventionally reported as 128 plus the signal number. The OOM killer terminates a process with signal 9 (SIGKILL, the uncatchable, unblockable kill signal) — and 128 plus 9 is 137. Seeing exit code 137 is a strong, fast hint that a process was killed by SIGKILL, though it’s worth being precise: 137 alone confirms a SIGKILL, not specifically that the OOM killer was the one that sent it.

The definitive confirmation: the OOMKilled flag

Docker itself tracks, separately from the raw exit code, an explicit boolean field on the container’s state specifically recording whether the kernel’s OOM killer was the cause of that container’s termination. This field is the single most direct, unambiguous confirmation available — where exit code 137 is a strong hint, this flag is Docker’s own explicit statement that it detected an OOM kill.

SignalContainer exit codeTypical meaning
SIGTERM (15)143A graceful termination request — the opposite of an OOM kill’s abruptness
SIGKILL (9)137An immediate, uncatchable kill — consistent with, but not proof of, an OOM event
SIGSEGV (11)139An application-level crash (segmentation fault), unrelated to memory limits
Normal application exitWhatever the application itself returnsA deliberate, self-initiated shutdown, not a kernel-forced one
i
Why exit code alone isn’t proof

SIGKILL can come from places other than the OOM killer — a manual forceful stop, an orchestrator’s own aggressive termination policy, or a host-level process manager. Exit code 137 should be treated as “go check the OOMKilled flag and the kernel log,” not as a conclusion on its own.

Why relying on exit code alone leads to false positives

It’s worth walking through a concrete false-positive scenario to make this concrete. An orchestrator performing a rolling update might forcefully terminate an old container instance that’s taking too long to shut down gracefully, sending exactly the same SIGKILL an OOM kill would use — producing the identical exit code 137, with no memory problem involved at all. Someone reading only the exit code in this scenario would reasonably, but incorrectly, suspect a memory issue, potentially spending real investigation time looking for a leak that was never there. Checking the OOMKilled flag first would have ruled this out in seconds.

7Reading the Kernel Log: What the Report Actually Contains

This is the richest evidence source available, and it’s worth understanding its structure conceptually, section by section, even without reproducing exact log syntax here.

Report section

Trigger context

Which process’s allocation request actually triggered the OOM killer to activate, and what memory scope (cgroup or system-wide) it was evaluated within.

Report section

Memory state snapshot

A breakdown of total, free, and reclaimable memory within the relevant scope at the moment of the kill, useful for confirming genuine exhaustion versus a miscalculated limit.

Report section

Candidate process table

A list of every process considered, generally including each one’s memory usage and computed badness score, letting you see exactly who else was “in the running.”

Report section

Selected victim

The specific process ultimately chosen and killed, identified by process name and ID, along with the cgroup it belonged to.

Mapping a kernel-level process back to a specific container

Because the kernel log reports process and cgroup identifiers rather than Docker container names directly, correctly diagnosing which container was affected sometimes requires matching the cgroup path or process ID mentioned in the kernel log against Docker’s own container listing from around that same timestamp — a small but important translation step between the kernel’s vocabulary and Docker’s own naming.

i
Timestamp correlation is essential

Because a host can run many containers, and the kernel log is host-wide rather than per-container, correlating the kernel log entry’s timestamp against the specific container’s own exit time — visible via Docker’s lifecycle events — is usually the fastest way to confirm you’re looking at the right event, especially on a busy multi-container host.

8Distinguishing OOM Kills from Other Failure Modes

A container that “just disappeared” has several plausible explanations, and confusing them wastes real debugging time chasing the wrong root cause.

Failure modeKey distinguishing evidence
Genuine OOM killOOMKilled flag is true; kernel log shows a matching OOM report at the same timestamp
Application-level crash (e.g. an unhandled exception or segfault)OOMKilled flag is false; application logs typically show an error or stack trace before exit
Health-check-triggered restartNo kernel OOM log entry; the orchestrator or Docker’s own health-check mechanism initiated the stop deliberately
Manual or orchestrator-issued stopA corresponding “stop” or “kill” event in Docker’s own event stream, initiated by an external command rather than the kernel
Host-level OOM (not container-scoped)Kernel log shows a system-wide OOM event, potentially affecting multiple unrelated containers simultaneously
Simple Analogy

It’s the difference between an employee who resigns and writes a farewell email (a graceful application exit), one who collapses from exhaustion with a doctor’s note afterward explaining exactly why (an OOM kill, evidenced by the kernel’s own detailed report), and one who’s simply missing with absolutely no record of what happened at all (a genuinely unexplained failure worth investigating further, rather than assuming it must be memory-related).

!
Common Mistake

Assuming every unexpected container restart is an OOM kill simply because “the container just disappeared.” Confirming the OOMKilled flag and a corresponding kernel log entry before treating a memory limit increase as the fix avoids solving the wrong problem — and potentially masking a real, unrelated bug behind a larger memory allowance.

9Common Causes Behind Real OOM Kills

Once an OOM kill is confirmed, the next question is always “why did memory usage actually get this high” — and the answer usually falls into a small number of recurring patterns.

A genuine memory leak

Memory usage climbs steadily and never comes back down, even during idle periods — a strong signal that the application is allocating memory it never releases, and the OOM kill is simply the inevitable endpoint of a slow, ongoing leak rather than a sudden event.

An undersized memory limit for legitimate peak usage

Memory usage rises and falls in a pattern that tracks real traffic or workload spikes, but the configured limit simply never had enough headroom for the application’s genuine peak footprint — here, the application itself may be behaving correctly, and the limit is what needs revisiting.

An unbounded in-process cache

An application-level cache that grows without any eviction policy or maximum size can look identical to a memory leak from the outside, even though the underlying code is working “as designed” — just designed without a ceiling that respects the container’s own limit.

A sudden, unexpected spike from a single large request or batch job

A single oversized request, file, or batch operation can push memory usage up sharply and briefly, triggering an OOM kill that doesn’t fit the steady-climb pattern of a leak at all — worth distinguishing from a leak, since the fix (bounding individual request or batch sizes) is very different.

Neighboring container overhead eating into shared expectations

In setups where multiple closely-related containers (such as an application and its sidecar) are reasoned about together, underestimating the sidecar’s own memory footprint when sizing the pairing’s combined budget is a frequent, easy-to-overlook contributor.

A change in traffic shape after a deploy, not a leak at all

Sometimes memory usage genuinely does climb and never come back down — not because of a leak, but because a recent deploy changed real, legitimate behavior: a new feature that keeps more data in memory per request, a changed batch size, a new dependency with a larger baseline footprint. This is worth ruling out by checking deploy timing against the trend’s start, since the fix here is understanding and accepting (or reverting) an intentional change, not chasing a leak that was never there.

10Trade-offs in Setting Memory Limits

Once the cause is understood, the practical response usually involves a memory-limit decision — and every choice here carries a real trade-off, not a universally correct number.

Tighter Limits

  • Catches genuine leaks and runaway growth earlier, before they can affect neighboring workloads on a shared host.
  • Encourages deliberate, well-understood memory sizing rather than open-ended assumptions.
  • Risk: legitimate, healthy peak usage can trigger unnecessary OOM kills if the limit is set without real headroom.

Looser Limits

  • Provides more buffer against legitimate spikes, reducing false-positive OOM kills for otherwise healthy workloads.
  • Risk: a genuine memory leak takes longer to surface, consuming more host resources before finally triggering a kill.
  • Risk: on a shared host, an overly generous limit for one container can pressure the memory available to its neighbors.

Why “just raise the limit” is a diagnosis, not always a fix

Increasing a memory limit after an OOM kill is sometimes the genuinely correct response — when investigation confirms the workload’s real, legitimate peak usage simply needed more headroom. But raising the limit without first confirming the cause risks masking a real leak behind a larger allowance, buying time before the same problem resurfaces at a higher, more resource-costly ceiling. The diagnostic steps in earlier chapters exist precisely to make this distinction before committing to either response.

The role of a soft reservation alongside a hard limit

Beyond the binary choice of raising or lowering a hard limit, a separate soft reservation value — a target the kernel leans toward respecting under general host pressure without being a hard ceiling the container can never exceed — offers a middle path for workloads with genuinely variable usage. Setting a reservation close to typical usage, with a hard limit further above it for real spikes, gives a workload legitimate room to breathe during normal peaks while still keeping an absolute, enforced boundary against runaway growth.

11Security Considerations

OOM kills aren’t purely an operational nuisance — a few genuine security angles are worth understanding.

Memory exhaustion as a denial-of-service vector

A container without a sensible memory limit at all is vulnerable to being deliberately or accidentally driven into consuming excessive host memory, potentially degrading or crashing unrelated workloads sharing that host — a properly configured limit turns an unbounded, host-wide risk into a contained, single-container OOM event instead.

Repeated OOM kills as an early warning sign

A container repeatedly hitting OOM kills shortly after receiving specific kinds of input is worth treating as a potential signal of a maliciously crafted request designed to trigger excessive memory allocation, not purely a performance tuning issue — the diagnostic process in this tutorial doubles as a first step in recognizing that pattern.

!
Common Mistake

Treating every OOM kill purely as an internal capacity-planning issue, without ever asking whether the memory spike correlates with a specific, potentially adversarial input pattern. Not every OOM kill has a security angle, but dismissing the possibility outright without checking closes off a genuinely useful line of investigation.

12Monitoring & Alerting Strategy

The best diagnosis is the one that happens before a kill, not after — proactive memory monitoring turns an OOM kill from a mystery into a confirmed, predicted outcome.

Reactive

Post-mortem log correlation

Everything covered so far in this tutorial — reading the kernel log and Docker’s own state after the fact to understand what already happened.

Proactive

Trend-based alerting

Watching a container’s memory usage as a percentage of its limit over time, and alerting well before it actually reaches 100%, catching a slow leak days before it forces a kill.

Proactive

Restart-count monitoring

Tracking how often a specific container restarts is a strong, workload-agnostic signal of a recurring OOM problem, even without inspecting memory metrics directly.

Both

Correlated dashboards

Combining memory trend graphs with kernel-log-derived OOM events on one timeline turns scattered evidence sources into a single, coherent view of what happened and when.

Setting sensible alert thresholds

A common early mistake in building proactive monitoring is alerting only at the same threshold the kernel itself enforces — effectively duplicating the OOM kill’s own timing rather than providing any real warning ahead of it. Setting a meaningfully earlier threshold, with enough margin to actually investigate and respond before the hard limit is reached, is what turns monitoring into genuine prevention rather than a slightly faster notification of the same failure.

i
The habit worth building

Treat every confirmed OOM kill as a prompt to check whether existing monitoring would have caught the underlying trend earlier — if it wouldn’t have, that’s usually a more valuable fix than adjusting the limit alone, since it protects against the next, not-yet-known cause of the same symptom.

13Design Patterns & Anti-patterns

A handful of recurring mistakes account for most of the frustration teams experience around OOM kills — usually because the underlying diagnosis was skipped in favor of a quick-seeming fix.

ANTI-PATTERN-01 Avoid
Problem

Relying on a container’s restart policy alone to “handle” repeated OOM kills, treating the automatic restart as if it were a fix rather than a symptom being papered over.

Why It’s Harmful

A restart loop driven by a genuine memory leak or undersized limit continues consuming resources and disrupting availability indefinitely — the restart policy keeps the container technically running, but never addresses why it keeps failing in the first place.

Correct Approach

Treat any container restarting repeatedly with the OOMKilled flag set as an active investigation, using the kernel log and memory trend data to identify a genuine root cause before considering the issue resolved.

ANTI-PATTERN-02 Avoid
Problem

Setting a container’s memory limit to an extremely generous, effectively unbounded value specifically to “make the OOM kills stop,” without ever revisiting whether the underlying usage pattern is actually healthy.

Why It’s Harmful

This trades a visible, contained, diagnosable failure for an invisible one — the workload can still be leaking or misbehaving, but now it silently consumes more and more host resources instead of surfacing the problem through a clear, investigable signal.

Correct Approach

Set limits based on genuinely observed, healthy usage with reasonable headroom, and treat a limit that needs to be raised repeatedly over time as a signal worth investigating on its own, not simply accommodating indefinitely.

Good pattern: treating the OOMKilled flag as a first-class alert condition

Wiring monitoring specifically around Docker’s own OOMKilled state — rather than only generic “container restarted” alerts — lets a team immediately distinguish a memory-related failure from every other kind of restart, skipping straight to the right diagnostic chapter of this tutorial instead of starting an investigation from scratch each time.

14Best Practices & Common Mistakes

A practical checklist distilled from everything covered so far.

Do

Confirm OOMKilled before assuming the cause

Check the explicit flag and correlate with the kernel log before treating any unexpected exit as a memory issue.

Do

Correlate timestamps across evidence sources

Line up the kernel log entry, Docker’s lifecycle event, and any memory-trend graph on one shared timeline before drawing conclusions.

Do

Distinguish a slow leak from a sudden spike

The shape of the memory trend leading up to the kill points toward very different fixes, so look at the trend, not just the final moment.

Avoid

Raising limits as a reflexive first response

Investigate the cause first; a higher limit is sometimes correct, but only after confirming the workload’s genuine, healthy footprint actually needs it.

Avoid

Ignoring restart-loop patterns

A container that keeps restarting is actively telling you something — treat a repeating pattern as more urgent than a single, isolated event.

Avoid

Assuming application logs will explain it

Design monitoring around the kernel-level and Docker-level evidence sources this tutorial covers, since application logs are frequently silent about their own OOM termination.

Do

Check deploy timing against the trend’s start

A memory trend that begins climbing right after a specific deploy points toward that change as the likely cause, whether it’s a genuine bug or an intentional but under-provisioned behavior change.

Avoid

Treating a single cause as universal

A leak-shaped trend and a spike-shaped trend call for different fixes — resist applying the same remedy to every OOM kill without first characterizing which pattern is actually present.

15A Step-by-Step Diagnostic Walkthrough

Pulling every earlier chapter together into one repeatable investigation sequence for the next unexplained container exit.

1

Check the exit code and OOMKilled flag first

A fast, cheap first step that either confirms or rules out an OOM kill before investing time anywhere else.

2

Find the matching kernel log entry

Locate the OOM report at the corresponding timestamp, and confirm it references the right container’s cgroup rather than an unrelated process on the same host.

3

Read the memory trend leading up to the event

Pull whatever historical memory metrics are available for the container and characterize the shape: a slow, steady climb, a sudden spike, or a repeating sawtooth pattern.

4

Check for a restart-loop pattern

Review the container’s recent restart history — a single isolated event and a tight, repeating loop point toward very different levels of urgency and different likely causes.

5

Match the pattern against common causes

Compare the observed trend shape against the recurring causes covered earlier — leak, undersized limit, unbounded cache, sudden spike, or sidecar overhead.

6

Decide on a limit change, a code fix, or both

Only after the previous steps should a decision be made — and a genuine root cause often points toward an application-level fix, a limit adjustment, or a considered combination of both. Document the reasoning alongside the change, so a future investigation into a similar symptom doesn’t have to repeat the same evidence-gathering from scratch.

16Real-World & Industry Examples

The diagnostic patterns covered here show up consistently across the broader container ecosystem, not just plain Docker hosts.

Kubernetes’ own OOMKilled reason field

Kubernetes surfaces essentially the same underlying signal Docker does, reporting a pod’s container status with an explicit OOMKilled reason after a kernel-level termination — the same cgroup-based mechanism covered throughout this tutorial, just presented through a different orchestrator’s own status reporting layer.

Cloud-hosted container platform monitoring

Major managed container platforms build dedicated dashboards and alerts specifically around memory-limit exhaustion events, reflecting how common and operationally significant this specific failure mode is across the industry, well beyond any single tool or platform.

Incident postmortems as a recurring case study

Publicly shared engineering postmortems from a wide range of organizations frequently cite an undiagnosed or misdiagnosed OOM kill as a contributing factor in a larger outage — often specifically because the team initially chased an application-level explanation before realizing the kernel had killed the process outright.

Database and cache vendor guidance

Memory-intensive systems like databases and in-memory caches frequently publish their own explicit guidance on properly sizing container memory limits against their internal memory usage, precisely because undiagnosed OOM kills against these particular workloads are such a common, high-impact support issue.

CI and build-system memory tuning

Continuous integration platforms, where build and test containers frequently run close to whatever memory ceiling is configured for them, are another environment where OOM-kill diagnosis shows up constantly in practice — a flaky-looking test failure is sometimes, on closer inspection, actually a build container being OOM-killed mid-run, only discoverable by checking exactly the evidence sources this tutorial has walked through.

17Reading cgroup Memory Statistics Conceptually

Beyond the kernel’s OOM report itself, the cgroup’s own detailed memory statistics — when captured by monitoring before an event — provide the richest picture of exactly how usage built up over time.

Breaking total usage down by category

A cgroup’s memory accounting doesn’t just report one aggregate number; it breaks usage down into distinct categories, each telling a different part of the story. Anonymous memory usage reflects the application’s own heap and stack allocations — the category most directly tied to a genuine leak. File-backed page cache usage reflects data read from files that the kernel is holding onto for efficiency, which is normally reclaimable relatively cheaply and thus less concerning on its own. Swap usage, where applicable, shows how much of the container’s memory pressure has already been offloaded to disk-backed swap space before the limit was finally reached.

Memory categoryWhat rising values usually indicate
Anonymous (heap/stack) memoryThe category most associated with a genuine application memory leak
File-backed page cacheOften benign and reclaimable, though large amounts can still edge a cgroup toward its limit under pressure
Swap usageA sign the container was already under real memory pressure before the final OOM event occurred
Kernel memory (where tracked)Overhead from kernel-side structures tied to the container’s own activity, worth distinguishing from application-driven growth
Simple Analogy

It’s the difference between a doctor being told only “the patient’s weight increased” versus being given a full breakdown of exactly which measurements changed and by how much. The aggregate number tells you something happened; the category breakdown tells you what kind of something, which is usually what actually points toward the right explanation.

Why this breakdown matters more before the kill than after

The kernel’s own OOM report captures a snapshot at the moment of the kill, but a cgroup’s detailed statistics, sampled repeatedly over time by ordinary monitoring, show the trend leading up to that moment — which category was actually driving the climb, and how long it had been climbing. This historical view is often more useful for distinguishing a leak from a legitimate spike than the single final snapshot alone, which is why proactive metric collection, covered earlier, pays off directly here.

i
A practical habit

When investigating a suspected leak, look specifically at whether anonymous memory usage recovers during periods of low load. A healthy application’s memory usage typically breathes — rising under load and falling back during quiet periods. Anonymous memory that only ever climbs, regardless of load, is a strong, specific leak signal.

18OOM Kills in Multi-Container and Sidecar Setups

When an application is deployed alongside a sidecar — as covered in earlier tutorials on the sidecar pattern — diagnosing an OOM kill gains an extra layer worth understanding specifically.

Which container’s cgroup actually hit the limit?

Depending on how memory limits are configured across a paired application-and-sidecar deployment, each container may have its own independent cgroup and limit, or in some deployment models the two may effectively share a combined budget. The very first question worth answering in a multi-container investigation is which specific container’s cgroup the kernel log actually references — it’s entirely possible for a sidecar’s own memory usage, not the application’s, to be the actual trigger behind an OOM event that superficially looks like “the service went down.”

Simple Analogy

It’s like investigating why a two-person team missed a deadline — the honest first step is figuring out which team member’s workload actually caused the bottleneck, rather than assuming it must have been the one whose name is on the project, since the quieter contributor’s own overload can just as easily be the real cause.

Sidecar overhead as a quiet contributor to application-side limits

As mentioned earlier in the common-causes chapter, teams sizing a paired deployment’s memory budget sometimes account generously for the application’s own needs while underestimating the sidecar’s baseline footprint, or vice versa — leading to an OOM kill that traces back to a budgeting gap between the two, rather than a genuine problem in either container’s own code.

Why single-cgroup diagnosis skills still apply directly

None of this changes the fundamental diagnostic approach covered throughout this tutorial — every technique here, from reading the kernel log to distinguishing a leak from a spike, applies identically to each individual container’s cgroup. The only addition in a multi-container setup is the extra step of first confirming precisely which of the paired containers the kernel log is actually reporting on, before applying everything else.

!
Common Mistake

Assuming an OOM kill in a sidecar-paired deployment must be the application’s fault simply because the application is the more visible, business-relevant component. Always confirm the specific cgroup referenced in the kernel log before assigning a root cause to either half of the pairing.

19Frequently Asked Questions

Q1Is exit code 137 always an OOM kill?

Not necessarily — 137 confirms a SIGKILL was sent, which is consistent with an OOM kill, but SIGKILL can also come from a manual forceful stop or another external source. Always confirm with the OOMKilled flag and kernel log before concluding it’s memory-related.

Q2Why doesn’t my application’s own logs show anything about the crash?

Because SIGKILL is immediate and uncatchable, the application has no real opportunity to log a final message before termination — this is expected, not a sign that logging is broken.

Q3My host has plenty of free memory — why did my container still get OOM-killed?

Per-container memory limits are enforced by the container’s own cgroup boundary, independent of the host’s total available memory. A container can be OOM-killed purely for exceeding its own configured limit, even while the host as a whole has abundant free memory elsewhere.

Q4Should I always raise the memory limit after an OOM kill?

Only after confirming the cause. If the workload’s genuine, healthy peak usage simply needed more headroom, raising the limit is correct; if the real cause is a leak or unbounded cache, raising the limit only delays the same failure at a higher cost.

Q5How can I tell which process was killed if my container runs several?

The kernel log’s victim section identifies the specific process by name and ID within the container’s cgroup — the killed process isn’t necessarily the container’s designated main process, just whichever process scored highest in the kernel’s badness calculation.

Q6Can an OOM kill happen even with swap enabled?

Yes — swap only delays an OOM kill by providing additional reclaimable capacity; once both RAM and the available swap allowance within the cgroup are genuinely exhausted, the OOM killer still activates as the final resort.

Q7Is a single OOM kill always worth a full investigation?

A single, isolated event tied to a known, unusual spike (a one-off large batch job, for instance) may not warrant deep investigation. A repeating pattern, or an OOM kill with no obvious explanation, is worth the full diagnostic walkthrough covered in this tutorial.

Q8In a paired application-and-sidecar deployment, how do I know which container was actually killed?

The kernel log’s cgroup reference identifies exactly which container’s boundary triggered the event — always confirm this directly rather than assuming based on which of the two seems more likely at a glance.

Q9Does file-backed page cache usage count toward my container hitting its memory limit?

Yes, it’s counted as part of the cgroup’s total usage, though it’s normally reclaimed relatively cheaply before an OOM kill becomes necessary — a cgroup dominated by reclaimable cache rather than anonymous memory is a different, generally less concerning situation than one dominated by anonymous memory alone.

20Summary and Key Takeaways

Diagnosing an OOM kill is less about any single command and more about knowing where the evidence for an abrupt, kernel-level decision actually gets recorded — because the process itself almost never gets a chance to explain what happened on its own. Confirming the event through Docker’s own OOMKilled flag, reading the kernel’s detailed report of exactly who was killed and why, and tracing the memory trend that led up to it together turn what initially looks like a mysterious, unexplained failure into a concrete, investigable root cause. From there, the right fix — a code change, a limit adjustment, or both — follows naturally from the evidence, rather than from a guess. The underlying skill transfers well beyond any single incident: once the evidence-gathering habit is built, every future unexplained container exit becomes a five-minute check rather than an open-ended mystery.

Key Takeaways

  • An OOM kill is an external, kernel-level decision — the application is terminated immediately by SIGKILL, with no opportunity to log its own death.
  • The evidence lives in several separate places — the kernel log, Docker’s OOMKilled flag and exit code, and the Docker events stream each contribute a different piece of the picture.
  • Exit code 137 is a hint, not proof — always confirm with the explicit OOMKilled flag and a matching kernel log entry.
  • Victim selection is based on a computed badness score — scoped to the container’s own cgroup when a per-container memory limit is set.
  • The shape of the memory trend points toward the cause — a slow climb suggests a leak or unbounded cache; a sudden spike suggests an oversized individual request or batch job.
  • Raising the memory limit isn’t automatically the fix — it’s the right response only once the underlying cause has actually been confirmed.
  • Proactive memory-trend monitoring beats reactive log-reading — the best diagnosis is often the one that catches the trend before a kill ever happens.