Introduction
Kubernetes scheduling is the process of placing pods on Kubernetes nodes, considering available resources and any defined rules (if they exist) for assigning pods to nodes. Kubernetes offers various techniques for scheduling pods onto nodes. The goal of these techniques is to maximize the efficiency of Kubernetes cluster usage, optimize the utilization of available resources, and ensure that workloads are highly available.
This blog will cover the following techniques of Kubernetes scheduling:
- Taints and tolerations
- Node selector
- Affinity and anti affinity
Taints and tolerations
Taints and tolerations work together to prevent pods from being scheduled on inappropriate nodes. The difference between taints and tolerations is that taints are applied to nodes, while tolerations are applied to pods. Taints are used to repel inappropriate pods from a node, whereas tolerations define which taints a pod will tolerate, meaning on which nodes the pod can be scheduled despite the presence of a taint.
How to taint a node:
kubectl taint nodes <node> key=value:NoSchedule|PreferNoSchedule|NoExecute
How to untaint a node:
kubectl taint nodes <node> key=value:NoSchedule-|PreferNoSchedule-|NoExecute-
It is important to understand the meaning of NoSchedule, PreferNoSchedule and NoExecute.
NoSchedule will not allow pods to be scheduled on the node if the pod does not tolerate that kind of taint.
PreferNoSchedule represents a soft version of NoSchedule where the scheduler will try to avoid scheduling a pod onto a node if it does not tolerate that taint but it is not guaranteed.
NoExecute jumps into action in the context of pods already running on the node. If the node gets NoExecution taint, pods that do not tolerate that kind of taint will be evicted from the node immediately. If pods tolerate that taint, they will keep running always on that node, except if the tolerationSeconds attribute is specified. In that case, the pod will be evicted from the node after a defined number of seconds exceeds.
How to add toleration to the pod:
tolerations: - key: "key" operator: "Equal|Exists" value: "value" effect: "NoSchedule|PreferNoSchedule|NoExecute" tolerationSeconds: <number of seconds> (if effect NoExecute is specified)
It is important to emphasize that Kubernetes adds some taints automatically depending on the node state. There are some of those taints:
- node.kubernetes.io/not-ready: Node is not ready.
- node.kubernetes.io/unreachable: Node is unreachable from the node controller.
- node.kubernetes.io/memory-pressure: Node has memory pressure.
- node.kubernetes.io/disk-pressure: Node has disk pressure.
- node.kubernetes.io/pid-pressure: Node has PID pressure.
- node.kubernetes.io/network-unavailable: Node’s network is unavailable.
- node.kubernetes.io/unschedulable: Node is unschedulable.
Examples
There are some examples where it is useful to use taints and tolerations, like when we have dedicated nodes for core applications, gpu applications, and other kinds of applications. It is useful to use taints and tolerations if we have nodes with special hardware, and it is required to run some pods on nodes with that kind of hardware.
The image below illustrates how the scheduler determines if a pod should be scheduled on the node. There is a node with taint that should repel all pods except ones which tolerate the core application taint. On the other side, there are two pods representing the core application and the application that we developed. Pod A does not tolerate the node’s taint, meaning it is not an appropriate application for the node, so pod A is not going to be scheduled onto the node. Pod B is a core application, meaning the application which node is designed for, so pod B is going to be scheduled successfully onto the node.
In the image below, it is illustrated how NoExecute affects pods which are already running on the node. Pod A tolerates taint on the node A and pod is scheduled successfully on the node (step 1). Step 2 shows tainting node with NoExecute effect, and in that case pod A will be evicted from the node because it does not tolerate new taint. In case that there is a tolerationSeconds attribute defined in the tolerations block, pod A will be running for the seconds specified and after that will be evicted.

When is the NoExecute taint important? Imagine that there is a DaemonSet in the cluster with replicas across all of the nodes and we want to have those replicas running during the whole node lifecycle, even if the node encounters an issue (eg, unreachable, not-ready). In this case, the DaemonSet replicas need tolerations for such taints to ensure they are not evicted from the node when these conditions occur.
Administrators of Kubernetes clusters do not have to worry about this because the node controller automatically adds such taints onto nodes and the DaemonSet controller automatically adds NoSchedule and NoExecute tolerations to the DaemonSet.
Node Selector
Node selector is one of the easiest ways to define where we want to schedule pods. This approach is taking node labels into consideration. Node selector is needed to be defined in pod configuration, where we say on which node the pod should be scheduled based on node labels.
How to label Kubernetes node:
kubectl label nodes <node-name> <label-key>=<label-value> (eg. kubectl label nodes core-node-1 region=europe)
How to set node selector:
nodeSelector: region: europe
If there are multiple node selectors, a node must have all of the specified labels for the pod to be successfully scheduled on it.
The image below shows the labels of nodes and pods. Pod A will not be scheduled because the node does not match all the labels defined in its node selector. In contrast, Pod B has all the required labels, so it will be scheduled on the node.

Affinity and anti affinity
In contrast to taints which are used to repel pods from the inappropriate nodes, affinity is used to attract pods to nodes. This approach offers us the possibility to define detailed rules for scheduling pods onto nodes, specifying how strict the rules should be, and enabling scheduling based on pod labels, in addition to node labels.
Node affinity
There are two types of node affinity:
- requiredDuringSchedulingIgnoredDuringExecution – represents hard rule. If a rule is not satisfied, the scheduler will not schedule a pod onto a node.
- preferredDuringSchedulingIgnoredDuringExecution – represents a soft rule. The scheduler will try to schedule a pod onto a node considering rules, but if the rule is not satisfied, the scheduler will schedule a pod onto the inappropriate node.
In the following example, there is a deployment with specified node affinity rules. Rule that must be met is that the application must be scheduled onto a gpu node (node with instance=gpu label). The second rule, that should not be strictly met, is that the application should be scheduled onto a node placed in Europe (node with region=europe label).
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu-application
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: gpu-application
template:
metadata:
labels:
app: gpu-application
spec:
containers:
- name: busybox
image: busybox
imagePullPolicy: IfNotPresent
command: ["tail", "-f", "/dev/null"]
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: instance
operator: In
values:
- gpu
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: region
operator: In
values:
- europe
Operator attribute can take one of the following values: In, NotIn, Exists, DoesNotExists, Gt, and Lt.
PreferredDuringSchedulingIgnoredDuringExecution supports the weight parameter, giving more importance to the rule while evaluating rules. Rules with the highest score will be prioritized when the scheduler makes a scheduling decision for the Pod.
Inter-pod affinity and anti affinity
Imagine that we want to schedule a pod to the node where it is already running a specific pod, or in contrast, that we do not want to schedule pods on the same nodes. This is possible by using inter-pod affinity and anti affinity, which takes into consideration pod labels already running on some nodes.
There are two types of inter-pod affinity and anti affinity:
- requiredDuringSchedulingIgnoredDuringExecution
- preferredDuringSchedulingIgnoredDuringExecution
Where this approach could be useful? It might be that we want to schedule pods, which communicate frequently, on the same node. Or we want to spread pods across all nodes in the cluster running in different data centers, regions, and so on.
This kind of scheduling will be shown in a basic example.
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-api
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
db: mysql
spec:
tolerations:
- key: "application"
operator: "Equal"
value: "true"
effect: "NoSchedule"
nodeSelector:
instance: application
containers:
- name: busybox
image: busybox
imagePullPolicy: IfNotPresent
command: ["tail", "-f", "/dev/null"]
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: db
operator: In
values:
- mysql
topologyKey: availabilityZone
The example above shows us how to force a user-api application to be scheduled onto a node in the same region where the database application is already running (database pod with the db=mysql label). An important parameter here is topologyKey, which defines at what level the affinity or anti-affinity should be applied in the infrastructure. Simply put, it tells Kubernetes how to group or separate pods based on a specific characteristic (topology). The value of topologyKey is actually the labels on the nodes.
Let’s look at the following example:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
tolerations:
- key: "core"
operator: "Equal"
value: "true"
effect: "NoSchedule"
nodeSelector:
instance: core
containers:
- name: busybox
image: busybox
imagePullPolicy: IfNotPresent
command: ["tail", "-f", "/dev/null"]
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx
topologyKey: region
The example above shows how to force the nginx application (in case there are multiple replicas) to be scheduled onto nodes in different regions to ensure that if one region fails, nginx will still be running in another region. This allows us to have as many Nginx replicas as we have regions.
It is important to note that for the pod anti-affinity rule requiredDuringSchedulingIgnoredDuringExecution, the admission controller LimitPodHardAntiAffinityTopology limits the topologyKey to kubernetes.io/hostname. You can modify or disable the admission controller if you want to allow custom topologies.
What if we want to have 6 Nginx replicas, but only 3 regions are available? In that case, we can use the soft version preferredDuringSchedulingIgnoredDuringExecution so that the scheduler can schedule more Nginx pods in one region. An example solution is shown below.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
tolerations:
- key: "core"
operator: "Equal"
value: "true"
effect: "NoSchedule"
nodeSelector:
instance: core
containers:
- name: busybox
image: busybox
imagePullPolicy: IfNotPresent
command: ["tail", "-f", "/dev/null"]
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx
topologyKey: region
Operator parameter could have one of the following values: In, NotIn, Exists, and DoesNotExist.
Node autoscaling
In Kubernetes, all scheduling approaches provide a powerful way to control pod scheduling on specific node types. By tainting nodes, you ensure that only workloads with matching tolerations can be scheduled on them. This is particularly useful for specialized workloads such as GPU-based machine learning, high-memory applications, or spot instance workloads.
Imagine that there are some pods that need to be scheduled but there are no available nodes for them. So in that case, we need to provision additional appropriate nodes for those pods. But what if we can make it automatically with a node autoscaler?
When combining all mentioned scheduling approaches with a node autoscaler, Kubernetes can dynamically provision the correct node types based on pending pod requests. If a pod remains in a Pending state due to missing node capacity, the autoscaler detects the need and scales up an appropriate node pool.
Also, what if we have more nodes than we need? Actually, we pay more for resources we do not really use. Node autoscaler could help us here with downscaling of unneeded resources.
With this pattern, you optimize resource usage, prevent unnecessary costs, and ensure that critical workloads always get the required infrastructure. This approach makes Kubernetes smarter and more efficient in managing workloads across heterogeneous node types.
Conclusion
Kubernetes scheduling provides powerful mechanisms to control where pods run, ensuring optimal resource usage, performance, and availability. Taints and tolerations allow nodes to repel certain pods unless they explicitly tolerate them, enforcing constraints at a broad level. Node selectors offer a simple way to schedule pods based on key-value labels, while node affinity and anti-affinity provide more flexible rules, allowing preferred or required placement based on labels. Inter-pod affinity and anti-affinity extend this logic to influence pod co-location or separation based on application needs. These features are crucial for achieving efficient, fault-tolerant deployments, offering fine-grained control over pod placement while ensuring high availability and workload isolation.
“Kubernetes scheduling” Tech Bite was brought to you by Faris Mušić, DevOps Engineer at Atlantbh.
Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.
