Prerequisites: kubernetes
Kubernetes gives a container two different memory numbers:
resources:
requests:
memory: 512Mi
limits:
memory: 1GiThe request influences scheduling. The scheduler uses it when deciding whether a node has room for the Pod. The limit is enforced at runtime through Linux control groups. They are not a minimum and maximum allocation handed to the process at startup.
The Kubernetes resource-management documentation describes the distinction: requests participate in placement, while exceeding an enforced memory limit can activate the kernel out-of-memory path and terminate the process.
What OOMKilled proves
Inspect the current and previous container state:
kubectl -n logging get pod loki-0 \
-o jsonpath='{.status.containerStatuses[?(@.name=="loki")].lastState}'
kubectl -n logging describe pod loki-0A terminated state with reason OOMKilled and exit code 137 proves the container crossed an enforced memory boundary and was killed. It does not identify which internal operation consumed the memory.
That distinction separates symptom from cause:
| Observation | Conclusion |
|---|---|
OOMKilled, exit 137 | the container exceeded its memory boundary |
| restart count increasing | its controller keeps recreating or restarting it |
memory limit 384Mi | the boundary was small |
| replay and large-flush logs immediately before death | the recovery path was the consuming operation |
Raising a limit can stop the symptom while leaving the cause unexplained. It becomes a sound change only when measured recovery and steady-state behavior justify the new budget.
Why requests still matter
A 512Mi request does not reserve a private block of 512 MiB for Loki. It tells the scheduler to account for that much capacity when placing the Pod. The process can use less, and it can use more until it reaches the limit or the node experiences broader pressure.
For a stateful service, the request should represent a realistic normal operating floor. A request far below normal use lets Kubernetes pack too much onto the node. A request equal to a rare recovery peak can waste scarce homelab capacity. Devata therefore uses a 512 MiB request and a 1 GiB limit after observing about 181 MiB at final steady state and roughly 400 MiB during the recovered backlog path.
Application ceilings belong below container limits
An application-level memory threshold and a container limit solve different problems:
application threshold: begin shedding, flushing, or slowing work
container limit: kernel-enforced last boundaryIf an application waits until 4 GB before applying replay backpressure but its container is limited to 384 MiB, the application never reaches its own safety behavior. The kernel kills it first.
The useful relationship is:
steady state < application recovery ceiling plus overhead < container limit < node capacityThe middle term is not simple arithmetic. Measure it. Compression, concurrency, garbage collection, caches, and storage latency all affect peak usage.
Measure more than the current point
Useful signals include:
container_memory_working_set_bytes{
namespace="logging",
pod="loki-0",
container="loki"
}kube_pod_container_status_restarts_total{
namespace="logging",
pod="loki-0",
container="loki"
}kube_pod_container_status_last_terminated_reason{
namespace="logging",
pod="loki-0",
container="loki",
reason="OOMKilled"
}Current memory alone can look normal between crashes. Correlate the time series with restart state and application logs from the seconds before termination.