RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
Kubelet PLEG Health Check Debugger
02. Root Cause Vector

Kubelet PLEG (Pod Lifecycle Event Generator) has stalled, causing the node to enter NotReady state and halting all pod scheduling and hea...

03. Rapid Prescription

Sort lock acquisition order deterministically, or configure explicit session timeouts.

Zero-Trust Client IDE/ Performance

Kubelet PLEG Health Check Debugger

Kubelet PLEG (Pod Lifecycle Event Generator) has stalled, causing the node to enter NotReady state and halting all pod scheduling and health reporting on that node.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(55L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
---
# WARNING: This is a legacy kubelet config for the pci-secure-zone cluster.
# TODO: Fix the PLEG health check workaround after the Q4 migration.
# DEPRECATED: Some settings from the old on-premise kubeadm setup.
#
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
address: 0.0.0.0
port: 10250
cgroupDriver: systemd
clusterDNS:
  - 10.96.0.10
# OLD: clusterDomain: cluster.local
# NEW: clusterDomain: my-registry.internal/legacy-api
clusterDomain: my-registry.internal/legacy-api
# VULNERABILITY: The imageGCHighThresholdPercent is set too high, causing disk pressure and starving the kubelet's runtime, which can delay PLEG list operations.
imageGCHighThresholdPercent: 99
imageGCLowThresholdPercent: 80
# TODO: Re-evaluate after the chaos engineering test.
eventRecordQPS: 0
# Synthetic secret: AWS access key for the legacy logging bucket.
env:
  - name: AWS_ACCESS_KEY_ID
    value: AKIAIOSFODNN7EXAMPLE
  - name: AWS_SECRET_ACCESS_KEY
    value: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Synthetic secret: Stripe test API key embedded in a redundant config block.
stripeConfig:
  apiKey: sk_test_4eC39HqLyjWDarjtT1zdp7dc
  endpoint: https://api.stripe.com/v1
# Redundant systemReserved block from the old "overcommit" policy.
systemReserved:
  cpu: "100m"
  memory: "100Mi"
# This is a workaround for the NFS mount timeouts, but it conflicts with the runtimeRequestTimeout.
runtimeRequestTimeout: 2m
# VULNERABILITY: The syncFrequency is set lower than the runtimeRequestTimeout, causing frequent sync operations that can block PLEG's relist when the runtime is under pressure.
syncFrequency: 30s
# Synthetic secret: Multi-line RSA private key for SSH bastion access (dummy).
sshPrivateKey: |
  -----BEGIN RSA PRIVATE KEY-----
  MIIEowIBAAKCAQEArX8sZ2L6NUh7pLQ4NtY6p7z6X6X6X6X6X6X6X6X6X6X6X6X6X6
  X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6
  X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X
  X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X
  X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X
  -----END RSA PRIVATE KEY-----
# Legacy health check settings from the "high-availability" overlay.
healthzBindAddress: 0.0.0.0
healthzPort: 10248
# VULNERABILITY: The fileCheckFrequency is set too low, causing excessive I/O and potentially delaying PLEG's periodic relist, leading to "PLEG is not healthy" errors.
fileCheckFrequency: 5s
# TODO: Remove after debugging the node NotReady issues.
# enableDebuggingHandlers: true
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

How to Fix Kubelet PLEG Not Healthy: Node NotReady Debugging Guide

Threat/Impact Level: CRITICAL | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–45 mins

TL;DR

  • What broke: Kubelet's PLEG loop stopped syncing container runtime state. The node flipped to NotReady, evicting or orphaning all pods scheduled on it.
  • How to fix it: Identify whether the root cause is a container runtime deadlock (containerd/dockerd hung), inotify descriptor exhaustion, or disk/memory pressure — then restart the offending service or tune kernel parameters.
  • Shortcut: Drop your kubectl describe node and journalctl -u kubelet output into the Client-Side Sandbox above to get an automated root-cause diagnosis and remediation script.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

Raw error — as it appears in journalctl -u kubelet -f:

E0610 03:42:17.123456    1234 pleg.go:418] PLEG is not healthy: pleg was last seen active 3m0.123456789s ago; threshold is 3m0s
W0610 03:42:17.123456    1234 node_lifecycle_controller.go:1501] Node <node-name> status is now: NodeNotReady

PLEG (Pod Lifecycle Event Generator) is the kubelet subsystem that polls the container runtime (via CRI) every plegRelistPeriod (default: 1s) and generates lifecycle events (started, died, etc.) for every container. It has a hard watchdog threshold of 3 minutes. If the relist loop hasn't completed within that window, kubelet marks itself unhealthy and the node controller marks the node NotReady.

Immediate consequence: All pods on this node lose their Ready condition. Deployments with PodDisruptionBudgets may block rollouts cluster-wide. Stateful workloads (databases, Kafka brokers) that were pinned to this node are now down.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is not a soft warning — this is a full node outage event.

Cascading failure chain:

  1. PLEG stalls → kubelet cannot reconcile container state → pods stuck in Unknown
  2. Node controller waits pod-eviction-timeout (default: 5 min) → starts force-evicting pods
  3. If the node has NoExecute taints added automatically, DaemonSets are also evicted
  4. If this node hosts a control-plane component (etcd, kube-apiserver in self-managed clusters) → cluster quorum risk
  5. Persistent Volumes with ReadWriteOnce remain attached to the dead node → new pods on replacement nodes cannot mount the PVC (the infamous "multi-attach" error follows)

Most common root causes ranked by frequency in production:

Root Cause Signal
containerd or dockerd goroutine deadlock crictl ps hangs; high D-state processes
inotify watch limit exhausted dmesg shows inotify_add_watch: No space left
Disk pressure (imagefs or nodefs full) kubectl describe node shows DiskPressure=True
Zombie/unkillable containers crictl ps shows containers stuck in UNKNOWN state
Kernel version bug with cgroups v2 Specific kernel + containerd version matrix

How to Fix It (The Solution)

Step 1: Triage in 60 Seconds

# Is the container runtime responding?
crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps

# Check for disk pressure
kubectl describe node <node-name> | grep -A5 Conditions

# Check inotify limits
cat /proc/sys/fs/inotify/max_user_watches
sysctl fs.inotify.max_user_instances

# Find zombie containers
crictl ps -a | grep -v Running

# Pull kubelet logs for the 10 minutes before the event
journalctl -u kubelet --since "10 minutes ago" | grep -E 'PLEG|relist|runtime'

Basic Fix — Restart the Container Runtime

If crictl ps hangs or returns errors, the runtime is deadlocked. This is the most common cause.

# For containerd
systemctl restart containerd
systemctl restart kubelet

# For docker (legacy)
systemctl restart docker
systemctl restart kubelet

# Verify node recovers
kubectl get node <node-name> --watch

⚠️ Restarting containerd will briefly interrupt running containers. On a node already in NotReady, this is acceptable. Do NOT do this on a healthy node without draining first.


Fix — inotify Exhaustion (Common on High-Density Nodes)

# /etc/sysctl.d/99-kubelet-inotify.conf

- fs.inotify.max_user_watches = 8192
- fs.inotify.max_user_instances = 128
+ fs.inotify.max_user_watches = 1048576
+ fs.inotify.max_user_instances = 512
sysctl -p /etc/sysctl.d/99-kubelet-inotify.conf

Enterprise Best Practice — Tune PLEG Relist Period and Add Runtime Liveness Probes

The default plegRelistPeriod of 1s is aggressive on nodes running 100+ containers. Tuning it reduces false-positive PLEG stalls caused by slow CRI responses under load.

# /var/lib/kubelet/config.yaml (KubeletConfiguration)

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
- # No relist tuning — using defaults
+ runtimeRequestTimeout: 15m
+ # Increase image pull / CRI operation timeout for high-density nodes
evictionHard:
-  nodefs.available: "100Mi"
-  imagefs.available: "1Gi"
+  nodefs.available: "500Mi"
+  imagefs.available: "5Gi"
+  nodefs.inodesFree: "5%"
# Node-level containerd config: /etc/containerd/config.toml

[plugins."io.containerd.grpc.v1.cri"]
-  max_concurrent_downloads = 3
+  max_concurrent_downloads = 6

[plugins."io.containerd.grpc.v1.cri".containerd]
-  # default snapshotter
+  snapshotter = "overlayfs"

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Node Problem Detector (NPD) — Deploy It If You Haven't

# Deploy node-problem-detector as a DaemonSet
# It surfaces PLEG and kernel issues as Node Conditions before kubelet gives up
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-problem-detector
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: node-problem-detector
  template:
    spec:
      containers:
      - name: node-problem-detector
        image: registry.k8s.io/node-problem-detector/node-problem-detector:v0.8.14
        securityContext:
          privileged: true

2. Alert Before the 3-Minute Threshold

# Prometheus alerting rule
- alert: KubeletPLEGDurationHigh
  expr: kubelet_pleg_relist_duration_seconds_quantile{quantile="0.99"} > 10
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "PLEG relist p99 > 10s on {{ $labels.node }} — PLEG stall imminent"

- alert: KubeletPLEGNotHealthy
  expr: kube_node_status_condition{condition="Ready",status="false"} == 1
  for: 1m
  labels:
    severity: critical

3. Enforce Node Capacity Limits via OPA/Gatekeeper

Prevent over-scheduling that causes container runtime saturation:

# OPA ConstraintTemplate to cap pods-per-node
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sMaxPodsPerNode
metadata:
  name: max-pods-per-node
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
  parameters:
    maxPods: 80  # Below kubelet default of 110 — leave headroom for system pods

4. Add to Your Node Provisioning Pipeline (Terraform/Ansible)

# In your node bootstrap userdata / cloud-init

+ cat > /etc/sysctl.d/99-k8s-node.conf <<EOF
+ fs.inotify.max_user_watches=1048576
+ fs.inotify.max_user_instances=512
+ kernel.pid_max=4194304
+ EOF
+ sysctl -p /etc/sysctl.d/99-k8s-node.conf

Bake this into your golden AMI / node image. A node that boots without these settings on a high-density cluster is a PLEG incident waiting to happen.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Will restarting containerd kill my running pods?
Yes, restarting containerd will cause a brief but total interruption of all containers on the node. This is because the node is already in a NotReady state due to the PLEG watchdog tripping, rendering the pods unschedulable. The root cause is a failure in the container runtime's ability to report container status within the required timeframe, often due to I/O contention, high container density, or a latent bug in containerd's internal state management. While a restart is the fastest manual fix, it's a blunt instrument that induces downtime and can mask deeper systemic issues. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that diagnoses the precise runtime failure and applies a targeted remediation without a full service restart, containing the blast radius.
Q2.Why does PLEG stall more often on nodes running 80+ pods?
PLEG stalls under high density because its synchronous relist cycle performs a full CRI `ListContainers` call. On a node with 80+ pods, this operation can exceed the 3-minute cumulative threshold if containerd is under I/O pressure from the overlay filesystem or if underlying storage is slow. Manually tuning parameters like `maxPods`, `runtimeRequestTimeout`, or migrating to faster storage is a complex, error-prone process that often introduces security vulnerabilities—misconfigured cgroup limits can lead to resource starvation attacks, and ad-hoc tuning lacks audit trails. Instead of risking a production outage with manual edits, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that performs safe, validated kernel and runtime parameter adjustments specific to your workload profile.
Q3.How do I tell if this is a containerd bug vs. a kernel/OS issue?
Diagnosing requires manual forensic steps: if `crictl ps` hangs, the fault lies in containerd or its shim; if it responds but PLEG is unhealthy, you must scour `dmesg` and `journalctl` for kernel OOM kills, inotify errors, or storage timeouts. Performing this triage manually across a fleet is a compliance nightmare—it lacks standardization, creates inconsistent security postures, and fails to meet audit requirements for change control. For reliable, scalable remediation, upgrade to the StackEngine CI/CD Automator (Team Edition). It enables on-prem, automated CI/CD pipeline integration, providing policy-enforced fixes, full audit trails, and bulk remediation across large mono-repos or entire clusters, turning reactive firefighting into a governed security operation.