Hitesh Sahu
Hitesh SahuHitesh Sahu
  1. Home
  2. ›
  3. posts
  4. ›
  5. …

  6. ›
  7. 4 0 Kubernetes Performance

Loading ⏳
Fetching content, this won’t take long…


💡 Did you know?

🐙 Octopuses have three hearts and blue blood.

🍪 This website uses cookies

No personal data is stored on our servers however third party tools Google Analytics cookies to measure traffic and improve your website experience. Learn more

Loading ⏳
Fetching content, this won’t take long…


💡 Did you know?

🦈 Sharks existed before trees 🌳.
kubernetes

    AI-AgenticAI

    AI-DeepLearning

    AI-GenAI

    AI-Infrastructure

    AI-Machine-Learning

    AI-Math

    AWS

    Azure

    Hobbies

    kubernetes
    • Kubernetes and Cloud Native Certification Path

    • Kubernetes: Control Loops, Scheduling, and GPUs

    • Kubernetes API Server Internals

    • etcd Architecture Explained

    • Kubernetes Scheduler Internals

    • Kubernetes Informers & Controllers Explained

    • Kubernetes Networking: Pods, Services, Ingress, and CNI

    • Kubernetes Storage: PV, PVC, StorageClass, and CSI

    • Helm: Kubernetes Package Manager

    • Cloud Native Observability: Prometheus, Grafana, OpenTelemetry, and Tracing

    • GPU Scheduling in Kubernetes: Device Plugins, GPU Operator & MIG

    • NVIDIA Network Operator: InfiniBand, SR-IOV, RDMA, and Multus

    • Dynamic Resource Allocation: The Future of GPU Scheduling in Kubernetes

    • Kubernetes Performance at Scale

    • Optimizing AI Inference at Scale: The Full Stack

    • Kueue: Kubernetes-Native Job Queuing and Quota Management

    • Multi-Node Distributed Training on Kubernetes

    • Kubernetes Topology Manager: NUMA-Aware GPU Scheduling

    • NVIDIA NIM: Optimized Inference Microservices on Kubernetes

    • GPU Autoscaling on Kubernetes: KEDA, HPA, and Cluster Autoscaler

    • kubernetes Index


    Management

    Programming

    Terraform

    Z_Appendix

    0-root

Cover Image for Kubernetes Performance at Scale
kubernetes

Kubernetes Performance at Scale

Kubernetes at hyperscale — official SLIs and SLOs, pod startup latency breakdown, watch storms, LIST scalability, API Priority & Fairness, etcd bottlenecks, scheduler throughput, horizontal API Server scaling, and benchmarking with ClusterLoader2 and KWOK.

Kubernetes
Performance
Scalability
APF
etcd
Control Plane
← Previous

Dynamic Resource Allocation: The Future of GPU Scheduling in Kubernetes

Next →

Kueue: Kubernetes-Native Job Queuing and Quota Management

Kubernetes Performance at Scale

Running Kubernetes with 100 nodes is very different from running it with 10,000 nodes.

The failures that emerge at hyperscale are not crashes — they are latency cliffs: operations that take 10 ms at 100 nodes take 10 seconds at 10,000 nodes, and the cluster appears healthy right until it doesn't.

This post covers where those cliffs are, how to measure them, and how the Kubernetes ecosystem addresses each one.


Kubernetes Scalability SLIs and SLOs

The Kubernetes project defines official Service Level Indicators (SLIs) and Service Level Objectives (SLOs) — measurable guarantees about control plane behavior at the certified scale ceiling.

Certified Scale Ceiling (as of Kubernetes 1.30)

Resource Limit
Nodes 5,000
Total Pods 150,000
Pods per node 110
Total Services 10,000
Total Namespaces 10,000

These are not hard limits — clusters can exceed them — but performance guarantees only apply within these bounds.

Official SLOs

SLI SLO
Mutating API call latency (POST/PUT/PATCH/DELETE) p99 < 1 s, excluding watch
Non-streaming read API call latency (GET/LIST) p99 < 30 s for large resources, < 1 s for small
Pod startup latency (scheduled + running, no image pull) p99 < 5 s
In-cluster DNS lookup latency p99 < 5 s
Scheduling throughput ≥ 100 Pods/s average

The pod startup latency SLO is the most operationally visible — it is what users feel when they scale a Deployment.


Pod Startup Latency — The Key SLI

Pod startup latency is not one number. It is a pipeline of stages, each of which can become the bottleneck independently.

flowchart LR

Submit["kubectl apply\n(t=0)"]

-->Admitted["API Server\nadmitted\n(t=50ms)"]

Admitted

-->Scheduled["Scheduler\nbinds to node\n(t=200ms)"]

Scheduled

-->ImagePull["Image pull\n(t=1–30s)"]

ImagePull

-->ContainerCreate["Container\ncreated\n(t+100ms)"]

ContainerCreate

-->Running["Pod Running\n(t=?)"]
Stage Typical duration What slows it down
API Server admission 10–100 ms Slow admission webhooks
Scheduler decision 50–500 ms Queue depth, filter/score at scale
Image pull 0–60 s Cold node, large image, registry rate limits
Container create 50–200 ms CNI plugin latency
Probe warm-up 0–30 s initialDelaySeconds on readiness probe

The SLO of p99 < 5 s excludes image pull time — it assumes the image is already cached. When benchmarking, always separate the two.

Key Prometheus metrics:

# Scheduling queue time
scheduler_e2e_scheduling_duration_seconds

# API Server admission latency
apiserver_admission_webhook_admission_duration_seconds

# Full pod startup (from creation to Running)
kubelet_pod_start_duration_seconds

Challenge 1: API Server Watch Storms

With 10,000 nodes and 50 controllers, there can be over 50,000 active watch connections to the API Server simultaneously.

A single busy Deployment — say, a rolling update touching 1,000 Pods — generates one event per Pod per status change. That is potentially tens of thousands of events that must be serialized and sent to every interested watcher.

flowchart LR

OneUpdate["1 Deployment\nrolling update"]

-->Events["~5,000 Pod events\n(Pending → Running → Ready)"]

Events

-->FanOut["Fan-out to\n50,000 watchers"]

FanOut

-->CPUSpike["API Server\nCPU spike"]

The Watch Cache

The Watch Cache is the primary defense. It holds a ring buffer of recent events per resource type, served entirely from memory.

flowchart LR

etcd

-->|"1 watch connection"| WatchCache["Watch Cache\n(in-memory ring buffer)"]

WatchCache

-->|"50,000 watch streams"| Clients["Controllers\nKubelets\nOperators"]

The API Server watches etcd once per resource type. All clients watch the API Server. This multiplexing is what keeps etcd alive at scale.

The Watch Cache also serves LIST requests from memory if the client passes resourceVersion=0 — skipping etcd entirely.

LIST Scalability and resourceVersion

LIST requests are a hidden scalability trap. A large LIST with no resourceVersion set forces a consistent read from etcd — serializing all objects, holding an etcd read transaction open, and loading everything into API Server memory.

# Consistent read — hits etcd, expensive
kubectl get pods --all-namespaces
# Internally: GET /api/v1/pods  (no resourceVersion → etcd read)

# Served from cache — cheap, slightly stale
kubectl get pods --all-namespaces -l app=web
# With resourceVersion=0: GET /api/v1/pods?resourceVersion=0  (cache)

Controllers using the SharedInformer always pass resourceVersion=0 for their initial LIST — this is why informer initialization does not hammer etcd even during a mass restart.

At large object counts (> 100,000 Pods), even cache-served LISTs can spike API Server memory because all objects must be serialized into the response. Pagination (the limit parameter) keeps any single response bounded:

# Pages of 500 objects instead of one massive response
kubectl get pods --chunk-size=500 --all-namespaces

Horizontal API Server Scaling

The API Server is stateless — it stores nothing locally. Multiple replicas can run behind a load balancer.

flowchart LR

Clients["kubectl\nControllers\nKubelets"]

-->LB["Load Balancer\n(e.g. HAProxy / kube-vip)"]

LB

-->AS1["kube-apiserver\nreplica 1"]

LB

-->AS2["kube-apiserver\nreplica 2"]

LB

-->AS3["kube-apiserver\nreplica 3"]

AS1 & AS2 & AS3

-->etcd["etcd cluster"]

Each replica maintains its own Watch Cache — they are independent. A client connecting to replica 2 gets the same data as one on replica 1 because both read from the same etcd cluster (with slight propagation delay).

At DGX Cloud scale, API Server replicas are sized by memory (Watch Cache grows with object count) and CPU (fan-out serialization). A cluster with 300,000 Pods may need API Server replicas with 64–128 GB RAM each.


Challenge 2: etcd Bottlenecks

Every write to the API Server eventually becomes a Raft log entry in etcd. At scale, write throughput and commit latency become the gating factor.

flowchart LR

APIServer["API Server\n(writes)"]

-->Leader["etcd Leader\n(Raft log)"]

Leader

-->F1["Follower 1"]

Leader

-->F2["Follower 2"]

Leader

-->|"fsync to WAL"| Disk["NVMe SSD\n(WAL fsync < 1ms p99)"]

The bottleneck is almost always disk fsync latency on the leader. Each committed log entry requires a WAL fsync. Network-attached storage (NFS, EBS gp2) regularly exceeds 10 ms per fsync — etcd expects < 1 ms.

At hyperscale, additional pressures:

Pressure Effect Mitigation
High write rate Longer Raft replication latency Batch more writes (API Server request coalescing)
Large object count etcd DB grows, compaction takes longer Regular compaction + defrag
DB size approaching 8 GB etcd refuses new writes Emergency compaction; increase --quota-backend-bytes
Leader election under load Brief write freeze (~2s) NVMe-backed etcd; dedicated etcd nodes

Dedicated etcd nodes with NVMe storage and no other workloads is the standard recommendation for clusters > 1,000 nodes.


Challenge 3: Object Count

Every Kubernetes object consumes memory in three places simultaneously:

flowchart LR

Object["Kubernetes Object"]

-->etcd["etcd\n(on disk + BoltDB page cache)"]

Object

-->WatchCache["API Server Watch Cache\n(in-memory)"]

Object

-->InformerCache["Controller Informer Cache\n(in-memory, per controller)"]

A cluster with 300,000 Pods, 300,000 ReplicaSets, and 500,000 Events might hold 5–10 million objects in etcd.

Why Events Are Dangerous

Events are the most proliferative object type. Every Pod lifecycle step generates events:

Scheduled → Pulling → Pulled → Created → Started → Readiness probe passed

Six events per Pod. 300,000 Pods. 1.8 million events — just for one rollout.

Events are also watched by monitoring tools, log aggregators, and alerting systems. One event write fans out to potentially thousands of watchers.

Mitigations:

  • Events have a default TTL of 1 hour (--event-ttl on kube-apiserver, default 1h)
  • Use events.k8s.io/v1 (the new Events API) which coalesces repeated events into a count field
  • Deploy a separate event sink (e.g., Event Exporter to ElasticSearch) and disable in-cluster event retention for large clusters

Object Lifecycle Management

For long-lived clusters, zombie objects accumulate: completed Jobs, finished Pods, stale ReplicaSets from old Deployments.

# Completed Jobs older than 1 hour (TTL controller)
kubectl get jobs --field-selector=status.completionTime  # prune manually

# Or use TTL controller on Job:
spec:
  ttlSecondsAfterFinished: 3600

The kube-controller-manager runs a garbage collector that follows owner references and deletes orphaned objects, but it has throughput limits — at very high object counts it can lag behind.


Challenge 4: API Priority and Fairness (APF)

At scale, a flood of kubectl get pods from a CI system can starve the scheduler and kubelets of API Server capacity.

APF classifies every request into a FlowSchema (who is making it, what operation) and routes it to a PriorityLevelConfiguration (how much concurrency it gets).

Priority Levels and Concurrency

flowchart LR

Request["Incoming Request"]

-->FS["FlowSchema\nmatch rules"]

FS

-->PL["PriorityLevel\n(concurrency share)"]

PL

-->|"Admitted"| Workers["API Server\nworker goroutines"]

PL

-->|"Queue full"| Drop["429 Too Many Requests"]

Built-in priority levels with their concurrency shares (percentage of total server concurrency):

Level Default share Users
exempt Unlimited system:masters, health probes
node-high 40% Kubelet node status, lease updates
system 30% kube-system controllers, scheduler
leader-election 8% Controller leader election
workload-high 30% Authenticated operators
workload-low 10% Regular user kubectl
global-default 10% Unauthenticated, catch-all

Concurrency shares are relative — if node-high is using only 20% of its share, the remainder is lent to other levels. APF is not a strict partition.

Diagnosing APF Throttling

# See if requests are being rejected (429)
kubectl get --raw /metrics | grep apiserver_flowcontrol_rejected_requests_total

# See queue depth per priority level
kubectl get --raw /metrics | grep apiserver_flowcontrol_current_inqueue_requests

# See which FlowSchemas exist and their priority levels
kubectl get flowschemas
kubectl get prioritylevelconfigurations

If workload-low is saturated and rejecting requests, tune its nominalConcurrencyShares up or reduce the flood source.


Challenge 5: Scheduler Throughput

At 100,000 pending Pods, the scheduler becomes the bottleneck even if the API Server and etcd are healthy.

What Limits Throughput

The scheduler runs a scheduling cycle for one Pod at a time, but runs multiple goroutines in parallel (controlled by --parallelism, default 16).

flowchart LR

ActiveQ["Active Queue\n100,000 Pods"]

-->G1["Goroutine 1\nFilter → Score → Bind"]

ActiveQ

-->G2["Goroutine 2\nFilter → Score → Bind"]

ActiveQ

-->G3["Goroutine 3\n..."]

ActiveQ

-->G16["Goroutine 16\n..."]

Throughput ceiling: ~100–200 Pods/second for a default scheduler with 5,000 nodes.

Key Tuning Knobs

Parameter Default Effect
--parallelism 16 Number of concurrent scheduling goroutines
percentageOfNodesToScore 0 (auto) Fraction of feasible nodes to score; auto-scales with cluster size
Filter plugin order Built-in Cheapest filters first eliminates most nodes early
--kube-api-qps 50 QPS to API Server for binding writes
--kube-api-burst 100 Burst above QPS for binding

The percentageOfNodesToScore auto-mode scores 50% of nodes at cluster size 100, dropping to ~5% at 5,000 nodes — balancing placement quality against throughput.


Benchmarking: ClusterLoader2 and KWOK

KWOK — Fake Clusters at Laptop Scale

Testing scheduler or controller behavior at 10,000 nodes normally requires 10,000 VMs. KWOK (Kubernetes WithOut Kubelet) simulates nodes and Pods without running any real workloads.

flowchart LR

KWOK["KWOK\n(fake node controller)"]

-->|"Creates"| FakeNodes["10,000 fake nodes\n(API objects only)"]

FakeNodes

-->API["API Server\n(sees real node objects)"]

API

-->Scheduler["Scheduler\n(schedules real Pods\nonto fake nodes)"]

API

-->Controllers["Controllers\n(reconcile normally)"]

KWOK nodes respond to kubelet heartbeat protocols — controllers and the scheduler behave identically to a real cluster. Only actual container execution is absent.

# Install KWOK
kubectl apply -f https://github.com/kubernetes-sigs/kwok/releases/latest/download/kwok.yaml

# Create 1,000 fake nodes
kwokctl create cluster --name=perf-test
kubectl apply -f - <<EOF
apiVersion: v1
kind: Node
metadata:
  name: fake-node-0001
  labels:
    type: kwok
spec:
  taints:
  - key: kwok.x-k8s.io/node
    effect: NoSchedule
EOF

ClusterLoader2 — Workload Benchmark

ClusterLoader2 is the Kubernetes project's official performance benchmark. It creates configurable workloads and measures SLI timings against the SLOs.

flowchart LR

CL2["ClusterLoader2\n(test definition YAML)"]

-->|"Creates"| Workloads["Pods / Deployments\n/ Services / Jobs"]

Workloads

-->Cluster["Cluster\n(KWOK or real)"]

Cluster

-->Metrics["Prometheus\nmetrics"]

Metrics

-->Report["SLI Report\n(p50/p90/p99 latencies)"]

A typical test definition:

name: load
namespace:
  number: 100
tuningSets:
- name: Uniformly1qps
  qpsLoad:
    qps: 1
steps:
- name: Create Deployments
  phases:
  - namespaceRange:
      min: 1
      max: 100
    replicasPerNamespace: 10
    tuningSet: Uniformly1qps
    objectBundle:
    - basename: test-deployment
      objectTemplatePath: deployment.yaml
- name: Wait for Pods to be running
  phases:
  - namespaceRange:
      min: 1
      max: 100
    replicasPerNamespace: 10
    tuningSet: Uniformly1qps
    objectBundle:
    - basename: test-deployment
      objectTemplatePath: deployment.yaml
      currentLoad:
        type: Running

ClusterLoader2 automatically collects and reports SLI measurements after each test:

API call latency:
  Metric: scheduler_e2e_scheduling_duration_seconds
  Percentile: 99
  Value: 487ms
  SLO: 1000ms ✅

Pod startup latency:
  Metric: kubelet_pod_start_duration_seconds
  Percentile: 99
  Value: 3.2s
  SLO: 5s ✅

Performance Metrics Reference

Metrics to monitor continuously in a production cluster at scale:

Metric Source Alert threshold
apiserver_request_duration_seconds API Server p99 > 1 s for mutating
apiserver_current_inflight_requests API Server Sustained > 80% of max
apiserver_flowcontrol_rejected_requests_total API Server Any increase
scheduler_e2e_scheduling_duration_seconds Scheduler p99 > 1 s
scheduler_pending_pods Scheduler Sustained > 1,000
etcd_disk_wal_fsync_duration_seconds etcd p99 > 10 ms
etcd_server_leader_changes_seen_total etcd > 3 per hour
etcd_mvcc_db_total_size_in_bytes etcd > 6 GB
kubelet_pod_start_duration_seconds Kubelet (per node) p99 > 5 s
apiserver_watch_events_total API Server Sudden spike

Key Takeaways

Scaling Kubernetes is not about adding more CPU or memory — it is about eliminating pressure on the control plane.

Bottleneck Root cause Solution
Watch storm Too many watchers receiving too many events Watch Cache fan-out; SharedInformers
LIST memory spike Full etcd read into API Server memory resourceVersion=0 cache reads; pagination
API Server saturation One request class starving others API Priority & Fairness
etcd write latency WAL fsync on slow disk NVMe-backed dedicated etcd nodes
Object count growth Events and completed jobs accumulating TTL controller; Event TTL; garbage collection
Scheduler throughput Single scheduling pipeline Increase parallelism; tune percentageOfNodesToScore
Pod startup regression New admission webhook or CNI change ClusterLoader2 SLI regression test in CI

At hyperscale, the control plane is a distributed system in its own right. Every component — API Server, etcd, scheduler, kubelet — has its own throughput ceiling, and they interact. The difference between a cluster that scales and one that doesn't is almost always instrumentation: if you can measure each SLI independently, you can find the bottleneck.

Hitesh Sahu
Written by Hitesh Sahu, a passionate developer and blogger.

Tue Jul 07 2026

Share This on

← Previous

Dynamic Resource Allocation: The Future of GPU Scheduling in Kubernetes

Next →

Kueue: Kubernetes-Native Job Queuing and Quota Management

kubernetes/4-0-Kubernetes-Performance
Let's work together
+49 176-2019-2523
hiteshkrsahu@gmail.com
WhatsApp
Skype
Munich 🥨, Germany 🇩🇪, EU
Playstore
Hitesh Sahu's apps on Google Play Store
Need Help?
Let's Connect
Navigation
  Home/About
  Skills
  Work/Projects
  Lab/Experiments
  Contribution
  Awards
  Art/Sketches
  Thoughts
  Contact
Links
  Sitemap
  Legal Notice
  Privacy Policy

Made with

NextJS logo

NextJS by

hitesh Sahu

| © 2026 All rights reserved.