How Kubernetes Chooses the Perfect Node for Your Spring Boot Application
In the previous article, we followed Sarah’s deployment from the moment she executed kubectl apply until the Deployment Controller created a ReplicaSet, and the ReplicaSet Controller created three Pod objects.
At that point, the cluster looked something like this.
Deployment
│
▼
ReplicaSet
│
▼
Pod A (Pending)
Pod B (Pending)
Pod C (Pending)
Notice something important.
Although the Pods exist inside Kubernetes, they are not running anywhere.
There is no JVM.
No Docker container.
No Spring Boot application.
No REST endpoints.
The Pods are simply waiting.
Waiting for what?
Waiting for one of the most intelligent components inside Kubernetes to make a decision.
That component is the Scheduler.
Today we’ll step inside the Scheduler and understand how Kubernetes decides exactly where every Spring Boot application should run.
A Common Misconception
Ask ten Java developers what the Scheduler does, and most will answer:
“It selects a worker node.”
Technically, they’re right.
But that’s like saying:
“Google Maps chooses a road.”
It certainly does—but only after analyzing traffic, road closures, speed limits, toll roads, accidents, construction work, estimated arrival time, and dozens of other variables.
Similarly, the Kubernetes Scheduler doesn’t randomly assign Pods to nodes.
Every scheduling decision is based on a surprisingly sophisticated evaluation process.
The Scheduler continuously asks:
“Which node can run this workload most efficiently while keeping the cluster balanced and resilient?”
This is far more than simple placement.
It’s intelligent orchestration.
Our Banking Cluster
Let’s revisit our banking platform.
Assume our production OpenShift cluster consists of six worker nodes spread across three availability zones.
Production Cluster
┌────────────────────────────────────────────┐
Zone A Zone B Zone C
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Worker 1 │ │ Worker 3 │ │ Worker 5 │
└──────────┘ └──────────┘ └──────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Worker 2 │ │ Worker 4 │ │ Worker 6 │
└──────────┘ └──────────┘ └──────────┘
Sarah’s Payment Service requires three replicas.
spec:
replicas: 3
The Scheduler now has one responsibility.
Find the best location for each Pod.
Not just a node.
The best node.
Why Can’t Kubernetes Pick Any Node?
Imagine you’re the operations manager of a hotel.
Three new guests arrive.
Would you randomly assign all three guests to Room 101?
Of course not.
You’d consider:
- Which rooms are available?
- Which rooms are already occupied?
- VIP preferences
- Room capacity
- Maintenance status
- Cleaning schedule
Only then would you assign rooms.
Scheduling Pods follows the same principle.
Random placement would lead to:
- CPU bottlenecks
- Memory exhaustion
- Uneven workload distribution
- Poor fault tolerance
- Increased downtime during node failures
Kubernetes avoids these problems through intelligent scheduling.
The Scheduler Watches for Pending Pods
Just like the Deployment Controller watches Deployments, the Scheduler continuously watches the API Server for Pods whose status is:
Pending
The moment Sarah’s ReplicaSet creates three Pending Pods, the Scheduler receives an event.
It now begins evaluating each Pod individually.
Notice something subtle.
The Scheduler doesn’t schedule Deployments.
It doesn’t schedule ReplicaSets.
It schedules Pods.
The Scheduling Pipeline
A scheduling decision isn’t a single step.
It’s a pipeline.
Conceptually, it looks like this.
Pending Pod
│
▼
Retrieve Available Nodes
│
▼
Filter Unsuitable Nodes
│
▼
Score Remaining Nodes
│
▼
Select Highest Scoring Node
│
▼
Bind Pod to Worker Node
Every Pod goes through this process.
For small clusters, this takes milliseconds.
Even in clusters containing thousands of nodes, Kubernetes performs this evaluation remarkably quickly.
Step 1 – Collect Candidate Nodes
The Scheduler first retrieves every worker node capable of running workloads.
Imagine the cluster currently looks like this.
| Node | CPU Free | Memory Free |
|---|---|---|
| Worker-1 | 6 vCPU | 14 GB |
| Worker-2 | 1 vCPU | 2 GB |
| Worker-3 | 8 vCPU | 20 GB |
| Worker-4 | 3 vCPU | 10 GB |
| Worker-5 | 7 vCPU | 18 GB |
| Worker-6 | 5 vCPU | 15 GB |
Initially, every node is considered a candidate.
The Scheduler hasn’t rejected anything yet.
Step 2 – Filtering
Now the real evaluation begins.
The Scheduler asks:
“Can this node run the Pod?”
This phase is called Filtering.
Think of it as an elimination round.
Nodes that fail mandatory requirements are removed immediately.
Typical filters include:
- Insufficient CPU
- Insufficient memory
- Node unavailable
- Node cordoned
- Taints
- Node affinity mismatch
- Unsupported operating system
- Unsupported architecture
- Volume constraints
- Network constraints
After filtering, only eligible nodes remain.
Example – CPU Requirements
Suppose Sarah’s Payment Service specifies:
resources:
requests:
cpu: "2"
memory: "4Gi"
This means Kubernetes must guarantee:
- 2 CPU cores
- 4 GB memory
Now consider Worker-2.
Worker-2
CPU Available
1 Core
The Scheduler immediately removes it.
Rejected
Reason
Insufficient CPU
Notice that Kubernetes isn’t checking current CPU utilization.
It checks allocatable requested resources.
This distinction is one of the most misunderstood concepts among developers.
Requests vs Actual Usage
Suppose a node contains:
8 CPU Cores
Applications have reserved:
7 CPU
Current CPU utilization is only:
15%
Can Kubernetes schedule another Pod requesting 2 CPUs?
No.
Why?
Because Kubernetes schedules based on requests, not current utilization.
The Scheduler assumes every application may eventually consume the resources it requested.
This conservative approach prevents future resource starvation.
Memory Filtering
Memory follows exactly the same principle.
Suppose the Payment Service requests:
memory: 8Gi
Worker-4 has:
Available Memory
6 GiB
The Scheduler rejects it immediately.
Unlike CPU, memory cannot be overcommitted safely for most workloads.
Choosing an undersized node would almost certainly result in an OutOfMemoryKilled (OOMKilled) container later.
Kubernetes prevents that risk during scheduling itself.
Taints and Tolerations
Not every worker node is intended for every workload.
Suppose the bank has dedicated nodes for Oracle databases.
Those nodes carry a taint.
database=true:NoSchedule
This tells Kubernetes:
“Do not place normal application Pods here.”
If Sarah’s Payment Service doesn’t declare a matching toleration, the Scheduler removes those nodes from consideration.
This protects critical infrastructure from accidental workload placement.
We’ll explore taints and tolerations in depth later in this series, but it’s important to understand that scheduling decisions aren’t based solely on hardware—they’re also driven by operational policies.
Node Affinity
Sometimes applications prefer specific nodes.
For example:
- GPU-enabled nodes
- SSD-backed nodes
- High-memory nodes
- Banking-compliant zones
- Country-specific infrastructure
Sarah may specify:
nodeSelector:
workload: banking
or use Node Affinity rules.
The Scheduler will only consider nodes matching those labels.
Again, incompatible nodes are eliminated before scoring begins.
After Filtering
Suppose filtering produces the following result.
Worker-1
Eligible
------------------
Worker-3
Eligible
------------------
Worker-5
Eligible
------------------
Worker-6
Eligible
Workers 2 and 4 were rejected.
Only four candidates remain.
Now comes the interesting part.
Which of these four is best?
That is the purpose of the scoring phase.
Step 3 – Scoring
Filtering answers:
Can this node run the Pod?
Scoring answers:
Which eligible node should run the Pod?
Multiple scoring plugins evaluate each remaining node.
Typical considerations include:
- Resource balance
- Least allocated resources
- Image locality
- Topology spread
- Inter-pod affinity
- Preferred node affinity
- Even distribution across zones
Every node receives a score.
Example:
Worker-1
Score = 84
----------------
Worker-3
Score = 92
----------------
Worker-5
Score = 88
----------------
Worker-6
Score = 79
The Scheduler selects the highest-scoring node.
In this example:
Worker-3
wins.
Notice something important.
The Scheduler didn’t choose the first available node.
It chose the best available node.
Binding the Pod
After selecting Worker-3, the Scheduler performs one final operation.
It updates the Pod specification.
Conceptually:
Before
Node = None
becomes
After
Node = Worker-3
This operation is known as binding.
At this point, the Scheduler’s responsibility is complete.
Its work is finished.
The Pod now has a destination.
But something is still missing.
Even though the Pod has been assigned to Worker-3:
- No Docker image exists on the node.
- No container is running.
- No JVM has started.
- Spring Boot hasn’t initialized.
- No HTTP server is listening on port 8080.
Those responsibilities belong to another critical Kubernetes component running on every worker node.
The kubelet has just noticed that a new Pod has been assigned to its node, and it’s about to transform a simple Pod definition into a running Spring Boot application.
In the next article, we’ll move from the Kubernetes control plane to the worker node itself and follow the kubelet as it pulls the Docker image, creates the container, starts the JVM, launches Spring Boot, and prepares the application for production traffic.