Pending means the pod exists as an object but no node accepted it. There is no container, no image to pull and no application to fail. It is a negotiation between what the pod asks for and what the cluster has, and the scheduler tells you exactly where they failed to agree.
`kubectl describe pod <name>` and straight to Events. The scheduler leaves a message like "0/12 nodes are available: 8 Insufficient memory, 4 node(s) had untolerated taint".
That message is a full tally: of twelve nodes, eight lack sufficient memory and four carry a taint this pod does not tolerate. Nothing needs guessing, and each category points at a different fix.
If the message says "0/0 nodes are available", the cluster has no available nodes at all and the problem is the autoscaler, not the pod.
**Insufficient cpu / Insufficient memory.** No node has free what the pod asks for in `requests`. Watch the trap: what gets compared is the request, not real consumption. A node can sit at 30% real usage and be "full" to the scheduler because the pods it already holds reserved everything.
**Untolerated taint.** The remaining nodes carry a mark — dedicated to another workload, GPU, under maintenance — and this pod brings no toleration. Fixed by adding the toleration or removing the taint, and it is worth understanding why the taint was there before removing it.
**Node affinity / selector does not match.** The pod asks for a label no node has. Usually a zone that no longer exists, an instance type that was retired, or a label written with one hyphen of difference.
**Unbound PVC.** The pod is waiting for a volume that could not be created. `kubectl describe pvc` explains it: a StorageClass that does not exist, or a volume in a different zone from the only node with room. A pod with a zonal volume cannot run in another zone.
**Topology spread or anti-affinity too strict.** A rule demanding one pod per zone in a two-zone cluster with three replicas leaves the third Pending forever. With `requiredDuringScheduling` the block is permanent; with `preferred` the scheduler relaxes it.
**The autoscaler cannot add nodes.** Account quota exhausted, the instance type out of capacity in that zone, or a maximum limit on the group. Autoscaler events say so and have to be read separately from the pod events.
When someone sets `requests` well above real consumption — "just in case", or by copying another service manifest — two bad things happen at once.
The first is Pending pods with no real cause: the scheduler believes nodes are full because it adds up reservations, not usage. A cluster at 30% effective utilisation can reject pods.
The second is that the autoscaler reacts to that fictional pressure and **adds nodes nobody will use**. You end up paying for capacity to hold reservations that correspond to no real work.
It is one of the most frequent sources of overspend in FinOps work, and it has the uncomfortable property of looking like a capacity problem: the team asks for more nodes and spend rises, when what needed reviewing was the manifest.
The way to detect it is comparing reserved against used per namespace. If the gap is large and sustained, the fix is right-sizing requests, not adding nodes.
With autoscalers that pick instance types by price and availability, like Karpenter, Pending becomes more dynamic and sometimes more confusing.
Normal behaviour is a pod sitting in Pending for a few seconds while a matching node is provisioned. That is not a failure: it is the system working.
It becomes a failure when the pod asks for something no available instance type offers — a CPU and memory combination that does not exist in the catalogue, or an architecture with no capacity — and the autoscaler tries and fails. Its own events explain it.
And a side effect: nodes appearing and disappearing faster makes per-host observability billing grow much quicker, because every node that existed counts.
A service that deployed fine and is now Pending has usually changed in one of three ways: its requests went up in a commit, an affinity or spread rule was added, or the cluster lost capacity because another team deployed something large.
The first two are in the manifest and have a date. The third is not in your service at all, and it is the hardest to see from inside: your pod does not fit because of something someone else did.
Telling "my manifest changed" apart from "the cluster changed" is what stops you chasing the problem in the wrong place.
Moonin keeps a continuous inventory of cluster resources and a revision history per service, requests and scheduling rules included. Seeing that your service requests went up in yesterday revision, or that another namespace grew, are two facts in the same place.
That answers whether the change was yours or the cluster, which is the fork where the most time gets lost.
What it does not do: it does not adjust requests, does not add nodes and does not modify scheduling rules. The only possible mutation in the entire product is HPA and replica adjustment by the Scaling Rules Agent, which is optional and ships disabled.
The demo is open and needs no form: you can look at how the resource inventory appears with reserved against used, before installing anything.
About usPricingSecurity and permissionsPartner programmevs Datadogvs Prometheus & Grafanavs New Relicvs Dynatracevs ELK and ElasticsearchCrashLoopBackOffOOMKilledImagePullBackOffPod stuck in PendingDocumentationDemoSign upContact usArguz, the consultancy
The scheduler writes it down. `kubectl describe pod <name>` and the Events section carries a message like "0/12 nodes are available: 8 Insufficient memory, 4 node(s) had untolerated taint". That is a full tally per category, and each category points at a different fix.
Because the scheduler compares `requests`, not real consumption. If the pods already on the node reserved all the memory, the node is full to the scheduler even if it is nearly empty in practice. The fix is right-sizing requests to real consumption, not adding nodes.
Yes, especially with autoscaling. A pod waiting while a matching node is provisioned is Pending by design. It becomes a problem when it stays there: if it is minutes, the pod and autoscaler events say what could not be obtained.
Usually a spread or anti-affinity rule that is too strict: for example demanding one pod per zone in a two-zone cluster with three replicas. With `requiredDuringSchedulingIgnoredDuringExecution` the block is permanent. Changing it to `preferred` lets the scheduler relax it when it cannot be satisfied.
Yes, indirectly. Inflated requests make the scheduler see full nodes that are empty, the autoscaler adds nodes to relieve pressure that does not exist, and you pay for capacity nobody uses. It is one of the most frequent sources of overspend, and it disguises itself as a capacity problem.