Monitor Kubernetes Pods and Nodes with kubectl top

Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package kubectl 1.36.3
Applies to Any host with kubectl configured; Kubernetes 1.31+ cluster with Metrics Server requirements met
Cert prep CKA · CKAD · CKS
Lab environment Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm
Privilege Normal user (no sudo required on the workstation)
Scope Metrics Server install and verification, kubectl top for nodes and Pods, per-container metrics, sorting and namespaces, comparing usage with requests and limits, a CPU stress lab workload, and troubleshooting. Does not cover Prometheus or Grafana, alerting, HPA or VPA, historical metrics, custom application metrics, or Metrics Server HA.
Related guides Kubernetes Pods and Pod lifecycle
kubectl command examples

kubectl top gives you a live read on CPU and memory without opening a dashboard. I use it when a node feels hot or a Pod might be starving its neighbors. This walkthrough installs Metrics Server on a kubeadm lab, verifies the Metrics API, then runs kubectl top on nodes, Pods, and individual containers while a small stress workload runs.


How kubectl top Collects Resource Metrics

kubectl top does not read cgroups on the node directly. The data path is:

text
Kubelet → Metrics Server → Metrics API → kubectl top
  • The kubelet exposes resource usage for the node and each Pod.
  • Metrics Server scrapes those kubelet endpoints and aggregates short-lived samples.
  • The Metrics API (metrics.k8s.io) exposes node and Pod metrics to clients.
  • kubectl top queries that API and prints human-readable columns.

CPU is reported as average core usage over the metric's sampling window. Memory is the working set measured when the sample was collected. Metrics Server aggregates these kubelet-provided values; it does not calculate them itself. Memory values such as 64Mi use Kubernetes binary units—Mi means mebibytes. The working set can include some file-backed cache and is not simply process RSS.

A few practical limits:

  • Metrics reflect recent usage, not a long history.
  • A Pod that just started may not have a sample until the pipeline publishes its first reading.
  • kubectl top is ideal for spot checks during troubleshooting, not dashboards, alerting, or capacity planning over weeks.

Install and Verify Metrics Server

Check for an existing installation

Before you install anything, confirm whether Metrics Server already runs in kube-system.

Check the Deployment:

bash
kubectl get deployment metrics-server -n kube-system

On a cluster without Metrics Server you see NotFound. After install you should see READY 1/1 and AVAILABLE 1.

List Metrics Server Pods:

bash
kubectl get pods -n kube-system -l k8s-app=metrics-server

Sample output on a working cluster:

output
NAME                              READY   STATUS    RESTARTS   AGE
metrics-server-6c9576559c-kvzxm   1/1     Running   0          2m1s

Confirm the aggregated APIService:

bash
kubectl get apiservice v1beta1.metrics.k8s.io

Sample output when the API is ready:

output
NAME                     SERVICE                      AVAILABLE   AGE
v1beta1.metrics.k8s.io   kube-system/metrics-server   True        72s

AVAILABLE=True confirms that the API aggregation layer can reach the Metrics Server API. It does not prove that Metrics Server has successfully collected node and Pod samples from every kubelet. Confirm the full path with a successful kubectl top node request.

If Metrics Server and the APIService are already working, skip the installation section and continue with node metrics.

Install Metrics Server 0.8.1

Metrics Server 0.8.x supports Kubernetes 1.31+, so the pinned v0.8.1 version is suitable for this lab. Kubernetes 1.31+ alone is not sufficient. Before you install, confirm the cluster meets these requirements:

  • API aggregation enabled
  • Kubelet webhook authentication and authorization
  • Network access from Metrics Server to each kubelet
  • Network access from the control plane to Metrics Server
  • Trusted kubelet serving certificates, or the lab-only insecure flag described below

Apply the pinned upstream manifest:

bash
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.8.1/components.yaml

Sample output (trimmed):

output
serviceaccount/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
service/metrics-server created
deployment.apps/metrics-server created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created

The command creates RBAC objects, a Service, the Deployment, and the v1beta1.metrics.k8s.io APIService. The v0.8.1 manifest sets a 15-second metric resolution and --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname.

Check whether the lab has the certificate error:

bash
kubectl logs -n kube-system -l k8s-app=metrics-server --tail=30

Fix kubelet certificate errors in the lab

On kubeadm labs, kubelet serving certificates often lack the node IP in SANs. Metrics Server logs then show kubelet x509 verification failures. Metrics Server requires trusted kubelet serving certificates—or --kubelet-insecure-tls for testing. The upstream manifest defines a /readyz probe that expects usable metrics, so the Deployment may remain unready until TLS is fixed or the testing-only flag is applied.

Only when the logs show kubelet x509 verification failures, apply:

bash
kubectl patch deployment metrics-server -n kube-system --type=json -p='[
    {
      "op":"add",
      "path":"/spec/template/spec/containers/0/args/-",
      "value":"--kubelet-insecure-tls"
    }
  ]'

Sample output:

output
deployment.apps/metrics-server patched

Do not append a second --kubelet-preferred-address-types flag; the manifest already sets it.

Do not treat --kubelet-insecure-tls as a production default. It disables kubelet certificate verification and is only a lab workaround. Fix kubelet serving certificates or trust on real clusters instead.

Verify the Metrics API and first sample

Then run:

bash
kubectl rollout status deployment/metrics-server -n kube-system --timeout=120s

Sample output:

output
deployment "metrics-server" successfully rolled out
bash
kubectl wait --for=condition=Available apiservice/v1beta1.metrics.k8s.io --timeout=120s

Sample output:

output
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io condition met

APIService Available=True confirms that the aggregated API can reach Metrics Server, but the first kubelet metrics collection may still be pending. Metrics Server normally collects on a short interval. Wait for the first node sample with bounded retries:

bash
for attempt in {1..36}; do
  kubectl top node >/dev/null 2>&1 && break
  sleep 5
done
bash
kubectl top node

Sample output:

output
NAME       CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)   
k8s-cp     691m         23%      3120Mi          47%         
worker01   40m          4%       1155Mi          62%

The final command either prints metrics or exposes the error after a maximum wait of three minutes. A successful kubectl top node response confirms the full kubelet → Metrics Server → Metrics API path; CPU does not have to be non-zero. Metrics Server is intended for recent resource signals and kubectl top spot checks, not precision monitoring.


Monitor Node Resource Usage

Read node CPU and memory

List every node:

bash
kubectl top node

By default, the node percentage columns use allocatable CPU and memory as their denominator. The absolute CPU(cores) and MEMORY(bytes) columns remain measured usage. --show-capacity changes the percentage denominator from allocatable resources to total capacity.

Column Meaning
CPU(cores) Current CPU usage across the node
CPU% Usage as a percentage of allocatable CPU
MEMORY(bytes) Current memory working set, displayed in units such as Mi
MEMORY% Working set as a percentage of allocatable memory, or capacity with --show-capacity

Restrict output to one node:

bash
kubectl top node k8s-cp

Sample output:

output
NAME     CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)   
k8s-cp   650m         21%      3122Mi          47%

Sort nodes

Sort nodes by CPU to find the busiest worker:

bash
kubectl top node --sort-by=cpu

Sample output:

output
NAME       CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)   
k8s-cp     650m         21%      3122Mi          47%         
worker01   93m          9%       1168Mi          62%

Sort by memory instead:

bash
kubectl top node --sort-by=memory

Allocatable versus capacity percentages

--show-capacity bases percentages on total node capacity rather than allocatable resources (which reserve space for system daemons):

bash
kubectl top node --show-capacity

Sample output:

output
NAME       CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)   
k8s-cp     650m         21%      3122Mi          46%         
worker01   93m          9%       1168Mi          59%

Memory percentages can drop slightly compared to the default view because total capacity is larger than allocatable memory.


Monitor Pod and Container Usage

Generate test CPU usage

Create a namespace and a Deployment with nginx plus a stress container so metrics move visibly.

bash
kubectl apply -f - <<'YAML'
apiVersion: v1
kind: Namespace
metadata:
  name: top-lab
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: stress
  namespace: top-lab
  labels:
    app: stress
spec:
  replicas: 1
  selector:
    matchLabels:
      app: stress
  template:
    metadata:
      labels:
        app: stress
        tier: demo
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.0
          resources:
            requests:
              cpu: 100m
              memory: 64Mi
            limits:
              cpu: 500m
              memory: 128Mi
        - name: stress
          image: busybox:1.36
          resources:
            requests:
              cpu: 100m
              memory: 32Mi
            limits:
              cpu: 500m
              memory: 64Mi
          command:
            - sh
            - -c
            - while :; do :; done
YAML

Sample output:

output
namespace/top-lab created
deployment.apps/stress created

Wait for the Deployment and the first metrics sample:

bash
kubectl rollout status deployment/stress -n top-lab --timeout=120s

Sample output:

output
deployment "stress" successfully rolled out
bash
for attempt in {1..36}; do
  kubectl top pod -n top-lab -l app=stress >/dev/null 2>&1 && break
  sleep 5
done
bash
kubectl top pod -n top-lab -l app=stress

The final command either prints metrics or exposes the error after a maximum wait of three minutes.

Store the Pod name for later examples:

bash
POD=$(kubectl get pod -n top-lab -l app=stress -o jsonpath='{.items[0].metadata.name}')

The CPU loop runs until the Deployment or namespace is deleted.

Monitor Pods by name, namespace, and label

List Pods in the default namespace:

bash
kubectl top pod

CPU is commonly shown in millicores (500m = half a core). These values are current usage, not configured requests or limits.

Query the lab Pod by name:

bash
kubectl top pod "$POD" -n top-lab

Sample output:

output
NAME                      CPU(cores)   MEMORY(bytes)   
stress-5756544dd7-z7v8h   500m         2Mi

List every Pod in the lab namespace:

bash
kubectl top pod -n top-lab

List Pods across all namespaces:

bash
kubectl top pod -A | head -8

Sample output:

output
NAMESPACE            NAME                                       CPU(cores)   MEMORY(bytes)   
calico-system        calico-apiserver-f786987bb-bt84s           4m           58Mi            
calico-system        calico-apiserver-f786987bb-x56xr           3m           48Mi            
calico-system        calico-kube-controllers-86bbc9c477-hfxkb   9m           60Mi            
calico-system        calico-node-hc7ph                          32m          136Mi           
calico-system        calico-node-sc7p2                          45m          108Mi           
calico-system        calico-typha-6545b7d97f-htqpt              5m           52Mi            
calico-system        csi-node-driver-54j2p                      1m           18Mi

Truncate with head when the cluster lists hundreds of system Pods.

Filter Pods using labels:

bash
kubectl top pod -n top-lab -l tier=demo

Sort Pods by CPU or memory

Sort by CPU to surface the hottest Pod first:

bash
kubectl top pod -n top-lab --sort-by=cpu

Switch to memory when you suspect a leak rather than CPU burn:

bash
kubectl top pod -n top-lab --sort-by=memory

With only one replica in top-lab, both commands return the same row; the flags matter when many Pods share a namespace.

View per-container metrics

Pod-level totals hide which container consumed the CPU. Use --containers to split multi-container Pods.

For one Pod:

bash
kubectl top pod "$POD" -n top-lab --containers

Sample output:

output
POD                       NAME     CPU(cores)   MEMORY(bytes)   
stress-5756544dd7-z7v8h   nginx    0m           2Mi             
stress-5756544dd7-z7v8h   stress   500m         0Mi

List every container in a namespace:

bash
kubectl top pod -n top-lab --containers

Per-container rows help when:

  • A Pod runs several containers and only one spikes
  • A sidecar consumes more than the main app
  • You need to justify separate limits per container

For sidecar layout patterns, see Kubernetes sidecar example.

Compare usage with requests and limits

kubectl top answers how much a workload uses right now. Requests and limits in the Pod spec answer how much the scheduler reserved and how much the kubelet may enforce.

High usage is not automatically wrong. Compare live metrics with configured resources before you raise limits.

Read current Pod usage:

bash
kubectl top pod -n top-lab

Inspect requests and limits on the same Pod:

bash
kubectl describe pod "$POD" -n top-lab | grep -A4 'Limits:\|Requests:'

Sample output:

output
Limits:
      cpu:     500m
      memory:  128Mi
    Requests:
      cpu:        100m
      memory:     64Mi
--
    Limits:
      cpu:     500m
      memory:  64Mi
    Requests:
      cpu:        100m
      memory:     32Mi

Pull the same fields from YAML:

bash
kubectl get pod "$POD" -n top-lab -o jsonpath='{.spec.containers[*].name}{"\n"}{.spec.containers[*].resources}{"\n"}'

Sample output:

output
nginx stress
{"limits":{"cpu":"500m","memory":"128Mi"},"requests":{"cpu":"100m","memory":"64Mi"}} {"limits":{"cpu":"500m","memory":"64Mi"},"requests":{"cpu":"100m","memory":"32Mi"}}

In the lab, the stress container should approach its 500m CPU limit, although sampled values will vary, while nginx stays near idle. For requests, limits, and QoS classes, see Kubernetes resources and requests.


Troubleshoot kubectl top

Symptom Likely cause Fix
Metrics API not available Metrics Server missing or APIService not Available Install Metrics Server; confirm Pod is Running and APIService is True
Metrics not available yet or pod metrics NotFound New Pod or node without a first scrape Wait and retry; on some clusters the first sample can take a few minutes
Metrics Server logs show TLS errors scraping kubelets Kubelet cert lacks node IP SANs Fix certs in production; use --kubelet-insecure-tls only on labs
kubectl top lists nodes but no Pods Wrong namespace, Pod not Running, or metrics lag Add -n; confirm STATUS Running; retry after a scrape interval
Values differ from Linux top Different measurement window and cgroup accounting Expect close trends, not identical numbers
Nodes report metrics but a new Pod does not Collection cycle not complete Wait and retry; verify container is running

When Metrics Server misbehaves, inspect its logs:

bash
kubectl logs -n kube-system -l k8s-app=metrics-server --tail=20

Confirm Metrics Server has ready EndpointSlice backends:

bash
kubectl get endpointslices \
  -n kube-system \
  -l kubernetes.io/service-name=metrics-server \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\tready="}{.conditions.ready}{"\n"}{end}'

Sample output:

output
192.168.5.33    ready=true

Empty output usually means the Metrics Server Pod is not ready yet.


What kubectl top Does Not Provide

kubectl top is a thin CLI over short-lived kubelet metrics. It does not give you:

  • Historical metrics or trend graphs
  • Dashboards or visualization
  • Alerts or notifications
  • Application-level metrics (HTTP latency, queue depth, etc.)
  • Long-term capacity planning data
  • Persistent monitoring storage

When you need retention and alerting, adopt a monitoring platform such as Prometheus with Grafana or your vendor's observability stack. That installation is outside this chapter.


What's Next


References


Summary

You verified whether Metrics Server is installed, applied the upstream manifest when needed, and confirmed the v1beta1.metrics.k8s.io APIService before trusting any kubectl top output. On the lab cluster, a kubelet TLS workaround was required; production clusters should fix serving certificates instead of skipping verification.

With metrics flowing, kubectl top node exposed allocatable CPU and memory percentages, while kubectl top pod and --containers separated Pod totals from per-container usage. The top-lab Deployment showed how one container can approach its CPU limit while a sibling container remains mostly idle, which is why per-container metrics matter.

Remember that these numbers are snapshots, not requests, limits, or history. Pair kubectl top with kubectl describe or Pod YAML when you decide whether to change resource settings, and use kubectl logs, Events and describe when usage looks wrong but the cause is still unclear.


Frequently Asked Questions

1. What does kubectl top show?

kubectl top prints recent CPU and memory usage for nodes, Pods, or containers. It reads from the Kubernetes Metrics API, which Metrics Server fills by scraping kubelets. The numbers are spot checks for troubleshooting, not historical charts or configured requests and limits.

2. Why does kubectl top say Metrics API not available?

Metrics Server is missing, not ready, or the v1beta1.metrics.k8s.io APIService is not Available. Install or repair Metrics Server, confirm its Pod is Running, and wait until the APIService reports Available before retrying.

3. How do I see per-container CPU and memory?

Run kubectl top pod pod-name --containers for one Pod, or kubectl top pod -n namespace --containers for every container in a namespace. Add -l key=value to filter Pods by label; the output already includes a POD column.

4. How long until a new Pod appears in kubectl top?

Metrics may be unavailable for a newly created Pod until the metrics pipeline collects and publishes its first sample. Wait and retry; on some clusters this can take a few minutes.

5. Does kubectl top show resource requests and limits?

No. kubectl top reports current usage only. Compare the numbers with requests and limits from kubectl describe pod or the Pod YAML before you change limit values.

6. Is --kubelet-insecure-tls safe for production?

No. It skips kubelet certificate verification so Metrics Server can scrape labs where kubelet serving certs lack IP SANs. In production fix kubelet certificates or trust configuration instead of disabling TLS checks.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)