SaveMyCert
Log in
5 of 5 free questions left today·for unlimited practice
Kubernetes Fundamentals

Kubernetes Scheduling: Requests, Node Affinity, Taints and Tolerations

12 min readKCNA · Kubernetes FundamentalsUpdated

Scheduling is the process by which Kubernetes decides which node runs each Pod: the kube-scheduler watches for Pods with no assigned node, filters the cluster down to the nodes that can feasibly run them, scores the survivors, and binds each Pod to the best-ranked node. The KCNA exam tests whether you can recognize each mechanism that shapes that decision: resource requests that reserve capacity, limits that cap it at runtime, nodeSelector and affinity rules that attract Pods to nodes, taints and tolerations that repel them, and DaemonSets that put one Pod on every eligible node. You will not be asked to author complex placement policies, but you will be asked what each mechanism does, which direction it works in, and why a Pod is stuck in the Pending state. This lesson covers exactly that recognition-level ground.

What you’ll learn
  • Describe the kube-scheduler's two-phase cycle of filtering feasible nodes and then scoring them
  • Distinguish resource requests, which drive scheduling decisions, from limits, which cap runtime usage
  • Recognize how nodeSelector, node affinity and pod affinity or anti-affinity constrain Pod placement
  • Explain how taints on nodes repel Pods and how tolerations allow, but never force, placement
  • Contrast DaemonSet scheduling with the count-based scheduling of Deployments
  • Diagnose why a Pod stays Pending and read the scheduler's event messages

How the kube-scheduler assigns Pods to nodes

The kube-scheduler is the control plane component responsible for placement. It continuously watches the API server for Pods that have no nodeName set, and for each one it runs a scheduling cycle with two phases. First comes filtering: the scheduler eliminates every node that cannot run the Pod at all. A node is filtered out if it lacks enough allocatable CPU or memory to cover the Pod's requests, if its labels do not satisfy the Pod's nodeSelector or required node affinity, if it carries a taint the Pod does not tolerate, or if a required host port is already in use. The nodes that survive are called feasible nodes.

Second comes scoring: the scheduler ranks the feasible nodes to pick the best one. Scoring considers signals such as how much free capacity a node would have left, whether the node already has the container image cached locally, and how well the node satisfies any preferred (soft) affinity rules. The scheduler binds the Pod to the highest-scoring node by writing that node's name into the Pod object.

Two boundaries matter for the exam. If filtering leaves zero feasible nodes, the Pod is not scheduled anywhere: it remains in the Pending state and the scheduler retries as the cluster changes. And the scheduler only decides placement; it never starts containers. Once a Pod is bound, the kubelet on the chosen node notices the assignment, pulls the images through the container runtime, and starts the containers. Scheduler picks, kubelet runs.

Resource requests and limits

Every container in a Pod can declare CPU and memory requests and limits. A request is the amount the container is guaranteed and, critically, the number the scheduler uses: during filtering, a node is feasible only if its remaining allocatable capacity covers the sum of the Pod's requests. The scheduler compares requests against what is already requested on the node, not against live usage, so a node full of idle-but-greedy Pods still counts as full. A limit is different: it plays no part in scheduling and is instead enforced at runtime on the node.

CPU is measured in cores or millicores, where 500m means half a core; memory is measured in bytes with suffixes such as Mi and Gi. The two resources behave differently when a container hits its limit. CPU is compressible: exceeding the CPU limit gets the container throttled, slowing it down but not killing it. Memory is not compressible: exceeding the memory limit gets the container OOMKilled and restarted.

AspectRequestsLimits
Who uses itkube-scheduler, at placement timekubelet and the kernel, at runtime
MeaningGuaranteed minimum reserved on the nodeHard ceiling the container may not exceed
CPU overageNot applicableContainer is throttled
Memory overageNot applicableContainer is OOMKilled

A typical declaration looks like this:

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi

Requests and limits also determine a Pod's Quality of Service class: Guaranteed when every container's requests equal its limits, Burstable when requests are set but lower than limits, and BestEffort when neither is set. Under node memory pressure, BestEffort Pods are evicted first.

nodeSelector: the simplest placement constraint

The most basic way to steer a Pod toward particular nodes is nodeSelector. Nodes carry labels, which are plain key-value pairs; some are added automatically by Kubernetes, such as kubernetes.io/hostname and the topology labels for region and zone, and administrators add their own to describe hardware or roles:

kubectl label nodes worker-2 disktype=ssd

A Pod then declares the labels a node must have:

spec:
  nodeSelector:
    disktype: ssd

The semantics are strict and simple. Every key-value pair in the nodeSelector must match the node's labels exactly, and it is purely a hard filter: a node either qualifies or it does not. There is no way to express a preference, no way to say a label's value should be one of several options, and no way to say a label must be absent. If no node in the cluster carries the required labels, the Pod is not placed on some second-best node; it simply stays Pending until a matching node appears.

For the exam, recognize nodeSelector as the entry-level mechanism: exact label match, hard requirement only. When a question mentions needing expressions like membership in a set of values, or a soft preference that the scheduler should try to honor but may ignore, the answer is node affinity, which exists precisely because nodeSelector cannot express those things.

Node affinity and anti-affinity

Node affinity is the expressive successor to nodeSelector. It lives under spec.affinity.nodeAffinity and comes in two flavors whose long names the exam expects you to recognize. requiredDuringSchedulingIgnoredDuringExecution is a hard rule: it behaves like nodeSelector in that a node failing the rule is filtered out and an unsatisfiable rule leaves the Pod Pending. preferredDuringSchedulingIgnoredDuringExecution is a soft rule: it only influences the scoring phase, each preference carries a weight from 1 to 100, and if no node satisfies it the Pod is scheduled anyway on the best available node.

The power comes from match expressions. Instead of exact equality only, affinity supports operators: In (the label's value is in a given set), NotIn, Exists (the label key is present, any value), DoesNotExist, plus Gt and Lt for numeric comparison. There is no separate anti-affinity field for nodes; node anti-affinity is simply an affinity rule written with NotIn or DoesNotExist, keeping Pods away from nodes with certain labels.

The suffix IgnoredDuringExecution is itself exam material: it means the rule is evaluated only when the Pod is scheduled. If a node's labels change afterward so that a running Pod no longer satisfies its required affinity, Kubernetes does not evict the Pod; it keeps running where it is. Affinity constrains placement, not continued residence.

A minimal hard rule looks like this:

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: disktype
          operator: In
          values:
          - ssd

Pod affinity and pod anti-affinity

Node affinity places Pods relative to node labels. Pod affinity and pod anti-affinity place Pods relative to other Pods: schedule me near Pods matching this label selector, or keep me away from them. Both come in the same required (hard) and preferred (soft) flavors as node affinity, with the same IgnoredDuringExecution behavior.

The distinctive ingredient is the topologyKey, which defines what near means. It names a node label, and all nodes sharing the same value for that label count as one topology domain. With topologyKey: kubernetes.io/hostname the domain is a single node, so anti-affinity means the Pods land on different nodes. With topologyKey: topology.kubernetes.io/zone the domain is an availability zone, so anti-affinity spreads Pods across zones.

The canonical use cases are worth recognizing on sight. Pod affinity co-locates workloads that benefit from proximity, such as placing a cache Pod in the same zone as the web Pods that read from it, reducing latency. Pod anti-affinity spreads replicas of the same application across nodes or zones so that the loss of one node or one zone cannot take down every replica at once; this is a standard high-availability pattern for databases and other replicated services.

One practical note the exam may echo: inter-Pod affinity is significantly more expensive for the scheduler to compute than node affinity, because it must consider the Pods already running across the cluster, so it is recommended for clusters up to a few hundred nodes rather than very large ones.

Taints and tolerations

Taints and tolerations work in the opposite direction from affinity. Affinity is a property of Pods that attracts them to nodes. A taint is a property of a node that repels Pods: once a node is tainted, no Pod may schedule onto it unless the Pod carries a matching toleration. The direction of each mechanism is a favorite exam discriminator:

  • nodeSelector and node affinity: set on the Pod, attract or restrict the Pod to certain nodes.
  • Taints: set on the node, repel all Pods that do not tolerate them.
  • Tolerations: set on the Pod, permit scheduling onto tainted nodes but do not attract or force the Pod there.

A taint has a key, an optional value, and an effect, applied with a command like kubectl taint nodes node1 gpu=true:NoSchedule. The three effects are: NoSchedule, new Pods without a toleration are not placed here, but Pods already running stay; PreferNoSchedule, a soft version the scheduler tries to honor but may violate; and NoExecute, the strictest, which blocks new placement and evicts Pods already running on the node that lack a toleration. A toleration may include tolerationSeconds to delay that eviction.

Kubernetes uses taints itself: control plane nodes typically carry node-role.kubernetes.io/control-plane:NoSchedule, which is why ordinary workloads do not land on them, and the node controller adds NoExecute taints such as node.kubernetes.io/not-ready to unhealthy nodes to drive eviction. Because a toleration only permits and never attracts, dedicating a node pool to special workloads takes both mechanisms: taint the nodes to keep everyone else out, and give the special Pods a toleration plus a node affinity so they actually go there.

DaemonSets versus normal scheduling

Most workload controllers think in counts: a Deployment with replicas: 3 wants three Pods somewhere, and the scheduler is free to place them on any feasible nodes. A DaemonSet thinks in coverage: it runs exactly one copy of a Pod on every eligible node in the cluster. When a new node joins, the DaemonSet controller creates a Pod for it automatically; when a node is removed, its Pod is garbage collected. You never set a replica count on a DaemonSet, because the node count is the replica count.

The classic use cases are per-node infrastructure agents, and the exam expects you to recognize them: log collectors such as Fluentd or Fluent Bit, node monitoring agents such as the Prometheus node exporter, storage daemons, and cluster networking components (CNI plugin agents and kube-proxy commonly run as DaemonSets). Anything that must observe or serve the node itself, rather than a fixed number of application replicas, fits the pattern.

Mechanically, the DaemonSet controller creates one Pod targeted at each eligible node and, in current Kubernetes, the default scheduler binds them using node affinity that the controller injects per Pod. Eligibility still respects the normal rules: a DaemonSet can use a nodeSelector or affinity to cover only a subset of nodes, such as only nodes labeled for logging. DaemonSet Pods are also given tolerations automatically for several node-condition taints, such as not-ready and unreachable, so a monitoring agent keeps running on a node that regular workloads are being evicted from, which is exactly when you need it most.

Scenario: diagnosing a Pending Pod

Here is the scenario the exam most likes to probe. You deploy a Pod that requests 3 CPUs to a three-node cluster. Each node has 4 allocatable CPUs, but existing workloads have already requested 2 CPUs on every node, leaving 2 free per node. Filtering removes all three nodes, because no single node has 3 unrequested CPUs, even though the cluster as a whole has 6 free. Requests are per-node, never pooled across the cluster. The Pod sits in Pending, and the first diagnostic step is:

kubectl describe pod myapp

The Events section at the bottom tells you exactly what filtering rejected, with messages like 0/3 nodes are available: 3 Insufficient cpu. Other tell-tale messages map to the mechanisms in this lesson: node(s) didn't match Pod's node affinity/selector means a nodeSelector or required affinity is unsatisfied, and node(s) had untolerated taint means every remaining node repels the Pod. The fixes follow directly: lower the request, add or enlarge nodes, label a node, relax the affinity, or add a toleration.

Two distinctions round out the picture. First, Pending is a scheduling problem, not a runtime problem: a Pod in CrashLoopBackOff or ImagePullBackOff was scheduled successfully and failed afterward on the node, so the scheduler is not the suspect. Second, Kubernetes supports priority and preemption: a Pending Pod with a higher PriorityClass can cause the scheduler to evict lower-priority Pods from a node to make room, which is the one built-in way a Pending Pod gets a node without the cluster otherwise changing.

Tip. KCNA questions on scheduling are pure recognition: which component assigns Pods to nodes (kube-scheduler), whether requests or limits drive placement, and what each mechanism does. Expect trigger phrases like 'repel Pods from a node' (taints), 'schedule onto a tainted node' (tolerations), 'one Pod per node' (DaemonSet), and 'required versus preferred' (hard versus soft affinity). The Pending-Pod scenario appears as a which-of-these-causes-this question, so tie Pending to filtering failures such as insufficient requests, unmatched selectors and untolerated taints.

Key takeaways
  • The kube-scheduler works in two phases: filter out infeasible nodes, then score the feasible ones and bind the Pod to the winner.
  • Requests drive scheduling and reserve capacity; limits are enforced at runtime and play no part in placement.
  • A container over its CPU limit is throttled; one over its memory limit is OOMKilled.
  • Affinity and nodeSelector attract Pods to nodes; taints repel Pods; a toleration permits but never forces placement on a tainted node.
  • requiredDuringScheduling rules are hard filters; preferredDuringScheduling rules only influence scoring and can be ignored.
  • NoExecute is the only taint effect that evicts already-running Pods; NoSchedule only blocks new ones.
  • A DaemonSet runs exactly one Pod on every eligible node; new nodes get the Pod automatically.
  • A Pod stays Pending when no single node passes filtering; kubectl describe pod shows the reason in its events.

Frequently asked questions

What is the difference between resource requests and limits in Kubernetes?

A request is the amount of CPU or memory reserved for a container and is what the kube-scheduler uses to decide whether a node has room for the Pod. A limit is a runtime ceiling enforced on the node: a container exceeding its CPU limit is throttled, and one exceeding its memory limit is OOMKilled. Limits never influence which node a Pod is scheduled onto.

Why is my Kubernetes Pod stuck in Pending?

A Pod stays Pending when the scheduler's filtering phase finds no feasible node. The usual causes are insufficient unrequested CPU or memory on every node, a nodeSelector or required node affinity that no node's labels satisfy, or taints on all remaining nodes that the Pod does not tolerate. Run kubectl describe pod and read the Events section, which states the exact reason, such as Insufficient cpu or untolerated taint.

What is the difference between nodeSelector and node affinity?

nodeSelector is a simple hard filter requiring exact label matches on the node, with no other options. Node affinity expresses the same idea with more power: match expressions with operators like In, NotIn, Exists and DoesNotExist, plus a choice between required rules that hard-filter nodes and preferred rules with weights that only bias scoring. Preferred affinity lets a Pod schedule somewhere else when no node matches, which nodeSelector can never do.

Do tolerations force a Pod onto a tainted node?

No. A toleration only removes the barrier a taint creates; it permits the Pod to schedule onto the tainted node but does not attract it there. A tolerating Pod can still land on any untainted node. To dedicate nodes to specific Pods you combine mechanisms: taint the nodes to repel everything else, and add both a toleration and a node affinity to the special Pods so they are allowed in and steered there.

What does the NoExecute taint effect do?

NoExecute is the strictest taint effect. Like NoSchedule it prevents new Pods without a matching toleration from being scheduled onto the node, but it additionally evicts Pods that are already running there without a toleration. A toleration can set tolerationSeconds to remain on the node for a grace period before eviction. Kubernetes itself applies NoExecute taints, such as node.kubernetes.io/not-ready, to drive Pods off unhealthy nodes.

How is DaemonSet scheduling different from a Deployment?

A Deployment asks for a fixed number of replicas and lets the scheduler place them on any feasible nodes, so several replicas may share a node. A DaemonSet instead runs exactly one copy of its Pod on every eligible node, with no replica count: when a node joins the cluster it automatically receives the Pod, and DaemonSets are the standard pattern for per-node agents like log collectors, monitoring exporters and networking components.

Test yourself on this topic
Practice questions with full explanations.
Practice now

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.