API Reference¶
The daemon serves HTTP on --listen, :9090 by default.
Unauthenticated
There is no authentication. It is intended to be reachable from the node it runs on, not from the cluster network. See Security.
Pre-1.0
The routes and the report JSON can still change shape, and have: the
report's process listing was named hogs before it became processes.
Every break is written up with its migration in the
changelog.
Routes¶
| Route | Returns |
|---|---|
GET /healthz |
ok once the process is up |
GET /readyz |
ready once the detector is attached and history is being kept |
GET /v1/status |
Operational snapshot |
GET /v1/events |
All reports, newest first |
GET /v1/events/{id} |
One report, or 404 |
GET /v1/events¶
Query parameters, all optional and combinable:
| Parameter | Effect |
|---|---|
namespace |
Only reports in this namespace |
pod |
Only reports for this pod name |
container |
Only reports for this container name |
limit |
At most this many reports, newest first |
A non-numeric or negative limit is a 400. An unknown id on
/v1/events/{id} is a 404. Both return a JSON error object.
Responses are served with Content-Type: application/json and
X-Content-Type-Options: nosniff, because reports carry values read from the
node's filesystem (cgroup paths, process command lines) and a browser must never
interpret those as markup.
Status schema¶
{
"detector": "ebpf",
"cgroupVersion": "v2",
"ready": true,
"reports": 3,
"skipped": 128,
"unattributed": 0,
"trackedCgroups": 47,
"uptimeSeconds": 3812.4,
"version": "v0.1.0",
"node": "worker-1",
"podCacheSynced": true,
"podsTracked": 10
}
| Field | Type | Meaning |
|---|---|---|
detector |
string | Active detection method: ebpf, poller, or fake |
cgroupVersion |
string | Hierarchy layout in use: v1, v2, or unknown |
ready |
bool | Mirrors /readyz |
reports |
uint | Post-mortems produced since start |
skipped |
uint | Kills discarded as belonging to no Kubernetes container |
unattributed |
uint | The subset of skipped that came from inside the kubepods tree |
trackedCgroups |
int | Containers that currently have sampled history |
uptimeSeconds |
float | How long the daemon has been running |
version |
string | Build the daemon was compiled from |
node |
string | Node whose pods are watched. Omitted when correlation is off |
podCacheSynced |
bool | Whether the informer finished its initial list |
podsTracked |
int | Pods on this node the cache holds |
unattributed is the one to alert on
skipped climbs on any busy node, because the probes see every kill on the
machine. It is not a fault and cannot be alerted on. unattributed is a real
Kubernetes OOM kill the daemon could not place and therefore never reported.
It should stay at zero.
Report schema¶
{
"id": "20260813T081522Z-0001",
"time": "2026-08-13T08:15:22Z",
"identity": {
"podUID": "3f0e2b6c-1a2b-4c3d-9e8f-0a1b2c3d4e5f",
"containerID": "9a8b7c6d5e4f...",
"kind": "container",
"qos": "Burstable",
"driver": "systemd",
"runtime": "containerd",
"cgroupPath": "/kubepods.slice/.../cri-containerd-9a8b....scope",
"namespace": "default",
"podName": "payment-api-6d5f78",
"containerName": "web-server",
"image": "payment:v1.2.0",
"resolved": true
},
"victim": {
"pid": 28145,
"nsPid": 17,
"comm": "node",
"cmdline": ["node", "./dist/garbage-collector.js"],
"rssBytes": 119537664,
"inferred": false,
"known": true
},
"source": "ebpf",
"killCount": 1,
"limitBytes": 536870912,
"peakBytes": 536870912,
"trajectory": [
{
"time": "2026-08-13T08:14:22Z",
"usedBytes": 432013312,
"limitBytes": 536870912,
"ratio": 0.8,
"pressureFull": 0.0
}
],
"processes": [
{
"pid": 28102,
"nsPid": 1,
"comm": "node",
"cmdline": ["node", "./dist/server.js"],
"rssBytes": 408944640
}
],
"groupKill": true,
"victimMatch": "nsPid",
"trend": {
"bytesPerSecond": 1048576,
"rSquared": 0.97,
"samples": 5,
"window": 60000000000,
"timeToLimit": 0,
"projected": true
}
}
Top level¶
| Field | Type | Meaning |
|---|---|---|
id |
string | Unique within one daemon lifetime, sorts chronologically |
time |
RFC 3339 | When the kill was observed. For the poller, up to one interval late |
identity |
object | The pod and container the kill belongs to |
victim |
object | The process the kernel killed |
source |
string | ebpf, poller, or fake |
killCount |
uint | The container's cumulative OOM kill count |
limitBytes |
uint | The memory ceiling that was breached. Zero when uncapped |
peakBytes |
uint | High-water mark, from memory.peak where the kernel exposes it |
trajectory |
array | Memory history leading up to the kill, oldest first |
processes |
array | The container's process list at report time, heaviest first, victim removed |
groupKill |
bool or null | Whether the cgroup is killed as an indivisible unit. null when it could not be read |
victimMatch |
string | Which identifier removed the victim from processes: hostPid, nsPid or none |
trend |
object | Growth analysis over the trajectory |
identity¶
| Field | Type | Meaning |
|---|---|---|
podUID |
string | Pod UID in canonical dashed form |
containerID |
string | Container ID without its runtime prefix. Empty for a pod scope |
kind |
string | container or pod |
qos |
string | Guaranteed, Burstable, BestEffort, or Unknown |
driver |
string | systemd, cgroupfs, or unknown |
runtime |
string | containerd, cri-o, docker, or unknown |
cgroupPath |
string | The path the scope was parsed from |
namespace |
string | From the API server. Empty when unresolved |
podName |
string | From the API server. Empty when unresolved |
containerName |
string | From the API server. Empty when unresolved |
image |
string | From the API server. Empty when unresolved |
resolved |
bool | Whether the cluster fields were populated |
kind: "pod" means the kill was charged to the pod slice rather than to any one
container, which is what a memory-backed emptyDir produces. There is no
container ID in that case. See Correlation.
victim¶
| Field | Type | Meaning |
|---|---|---|
pid |
int | Host-namespace process ID |
nsPid |
int | PID as seen inside the container, which is what application logs show |
comm |
string | The kernel's 15-character executable name |
cmdline |
array | Full argument vector. Omitted when the /proc read lost the race |
rssBytes |
uint | Resident memory at, or shortly before, death |
inferred |
bool | True means deduced from which process vanished, not named by the kernel |
known |
bool | False means no victim was identified at all |
trajectory[]¶
| Field | Type | Meaning |
|---|---|---|
time |
RFC 3339 | When the reading was taken |
usedBytes |
uint | Memory in use |
limitBytes |
uint | The ceiling in force. Zero when uncapped |
ratio |
float | usedBytes / limitBytes, in [0,1] |
pressureFull |
float | The memory.pressure full ten-second average |
processes[]¶
| Field | Type | Meaning |
|---|---|---|
pid |
int | Host-namespace process ID |
nsPid |
int | PID as seen inside the container |
comm |
string | The kernel's 15-character executable name |
cmdline |
array | Full argument vector. Omitted when unreadable |
rssBytes |
uint | Resident set size |
This is a snapshot, not a survivor list
Only when groupKill is explicitly false are the two the same thing. When
it is true the kernel is killing every process in the cgroup, so this is
whatever was still readable mid-teardown: entries are missing, and resident
sizes are already collapsing towards zero. When it is null, which of the
two this is was never established.
What it never contains is the victim itself.
groupKill¶
Reflects memory.oom.group on the cgroup the report is attributed to. containerd
sets it on the container scope, so on almost every current cluster it is true and
an OOM takes the whole container down.
It has three states, not two:
| Value | Meaning |
|---|---|
true |
The kernel killed every process in the cgroup. The container is gone |
false |
The kernel killed only the process it selected. The rest kept running |
null |
The setting could not be read. Neither of the above was established |
Do not treat null as false
A JSON consumer that reads a missing or null groupKill as false will report
that a container survived a kill that nothing observed. null is the honest
answer for a cgroup that was destroyed before it could be read, which is what
group kill does to the container being described.
Both true and false come from an actual read of memory.oom.group. The
detectors take that reading as early as they can: the eBPF probe fires on entry
to oom_kill_process, before SIGKILL is delivered, so it reads the cgroup while
it still exists. The poller learns of a kill from a counter on its next pass and
is more often left with null, which is the same difference in confidence that
victim.inferred records.
victimMatch¶
Which identifier removed the victim from processes.
| Value | Meaning |
|---|---|
hostPid |
The host-namespace PID matched. The daemon and the kernel number processes the same way |
nsPid |
Only the container-namespace PID matched. They do not, which is normal under a nested runtime such as kind |
none |
Neither matched |
none is expected when victim.known is false: there was no victim to remove.
With a known victim it is a fault, and it means processes may still name the
process the kernel killed. The terminal renderer says so; nothing else in the
output would give it away, because the entry carries a plausible PID and a
plausible size.
nsPid is worth reading when the numbers in a report look unrelated to each
other. It says victim.pid is a global PID from the kernel while the PIDs in
processes come from a different namespace, so the two are not comparable.
trend¶
| Field | Type | Meaning |
|---|---|---|
bytesPerSecond |
float | Fitted growth rate. Negative means memory is falling |
rSquared |
float | Goodness of fit in [0,1]. A flat line yields 1 |
samples |
int | How many readings informed the fit |
window |
int | Nanoseconds the fit covers |
timeToLimit |
int | Nanoseconds until usage reaches the limit |
projected |
bool | Whether timeToLimit was computed at all |
timeToLimit is meaningful only when projected is true. It is false when
memory is flat or falling, the cgroup is uncapped, or the window is too short to
fit a line.