What happens between kubectl apply and a running container
Eleven control-plane events happen between typing the command and the container running. Each is a small, narrowly scoped piece of work triggered by a change in watched state.
The Kubernetes control plane and what runs where
The API server, scheduler, controllers, and etcd, and their jobs.
Kubernetes pod creation is a sequence of eleven events involving the API server, etcd, the scheduler, the kubelet, and the container runtime. From kubectl apply to a running container typically takes 5–30 seconds. Each step is a separate watch-driven event, so knowing the sequence helps a lot when debugging.
Kubernetes' design is fairly flat. Four kinds of components matter for pod creation: the API server, etcd, a fleet of controllers (the scheduler being one), and per-node kubelets. They do not call each other directly, and every interaction goes through the API server.
Validates and admits every request
The only thing that reads or writes etcd. Authenticates, authorizes, mutates, validates, persists, and then broadcasts. Every controller is a watch client.
Stores the cluster’s desired state
Strongly consistent key-value store, Raft-replicated across 3 or 5 members. Holds every cluster object. If etcd loses quorum, the cluster goes read-only.
Drive actual state toward desired
Independent loops watching specific resources and nudging reality toward the declared spec. The scheduler, deployment, replica-set, endpoint and kubelet controllers all have the same shape: watch, then reconcile.
How the watch protocol keeps components in sync
Components watch the API server for changes instead of polling.
Every controller in Kubernetes opens a streaming HTTP connection to the API server with ?watch=1 and stays connected forever. Each event (ADDED, MODIFIED, DELETED) streams down as it happens. Restart a controller? It does a full LIST first to bootstrap, then resumes the watch from the last resourceVersion it saw.
Because of this, Kubernetes reacts to changes quickly without polling. When the scheduler assigns a pod, the kubelet on that node usually learns of it within milliseconds. Nobody tells it; it was already watching, and the API server pushed the change down its open connection.
A 5,000-node cluster has 5,000 kubelets. None of them needs every pod event in the cluster. Each kubelet asks for fieldSelector=spec.nodeName=node-X, so the API server filters server-side and only pods bound to that node stream to that kubelet. Total watch traffic grows linearly with the number of pods rather than with pods times nodes.
The eleven events that create a pod, in order
From the API write to the container running.
The simulator below plays a single kubectl apply -f pod.yaml from the command landing on the API server through the container running on node-3. It has five lanes and eleven hops. Use + Show wire bytes to see the payload at each step.
Everything starts with one HTTP POST. kubectl is a thin client that turns YAML into JSON and pushes it to the API server. It does no scheduling or validation of its own, so every decision is made in the cluster.
How the scheduler picks a node: filter, then score
First rule out nodes that can't fit, then rank the ones that can.
Every unscheduled pod goes through two passes in the default scheduler: filter (predicates that rule nodes out), then score (priorities that rank the rest). The highest-scoring node wins.
Is this node even possible?
Resource fit (does it have CPU/memory free?), volume attachment, taints, node selectors, affinity rules. A node that fails any predicate is removed from consideration.
Among the eligible, which is best?
Least-requested (prefer less-loaded), image-locality (already-cached image saves a pull), topology-spread (keep replicas across zones), inter-pod affinity. Each scores 0–100; the weighted sum picks the winner.
Splitting the work into filter then score makes the scheduler pluggable. You can write a custom scheduler plugin and add it to either pass. The scheduling framework calls the plugins in order. A single plugin rejecting a node ends filtering for that node, while every plugin contributes to scoring.
The kubelet: the agent that runs pods on each node
It takes the assigned pod spec and makes the container real.
The kubelet is another controller: it watches pods and reconciles toward the desired state. Here, reconciling means making Linux processes exist. The kubelet does not start a container itself; it tells the container runtime to.
The runtime (containerd, cri-o, historically Docker) speaks a gRPC interface called the Container Runtime Interface. Behind that interface, runc (or kata, gvisor, youki) clone()s the process into a fresh set of Linux namespaces, applies cgroups, mounts the rootfs, and execs the container command.
- CRI
RunPodSandbox
Creates the pause container, a tiny process that holds the pod's network namespace. Every container in the pod joins this sandbox. The CNI plugin attaches the network here, and the IP belongs to the sandbox, not the workload.
- CRI
PullImage · CreateContainer · StartContainer
Three separate gRPC calls. The image is fetched and unpacked, a writable overlay is stacked on top of the read-only layers, and the runtime forks the entrypoint into the sandbox's namespaces. Stdout and stderr go back through the runtime and the kubelet to the API server's log endpoint.
- CNI
Network setup
The CNI plugin (Calico, Cilium, AWS VPC CNI, Flannel) is invoked once per sandbox. It allocates an IP from the pod CIDR, creates a veth pair, attaches one end to the sandbox's network namespace and the other to a host bridge / tunnel / eBPF program. The IP is returned to the kubelet for the status update.
- probes
Liveness + readiness
Once started, kubelet begins firing the configured probes. Readiness failure removes the pod from Service endpoints (no traffic) but leaves it running. Liveness failure kills and restarts the container. Misconfigured probes are a common cause of "my pod keeps restarting."
Reconciliation loops keep running
The eleven steps above are not a procedure, and nothing "calls" them in order. Each component watches some piece of state and does its small job whenever that state changes. Pod creation is one path through that watch graph.
When you delete a pod, the kubelet sees the deletion event, gracefully terminates the container, removes the network attachment and deletes the sandbox. When a node dies, the node-lifecycle controller sees the heartbeat stop, marks the node NotReady, and eventually evicts the pods. The deployment controller notices and recreates them, and the scheduler places them. It is the same loop with a different starting state.
A procedural system (do A, then B, then C) has no clean resumption point when a step fails. A reconciler has no sequence to resume. Drift from desired state, in any direction, is simply the next reconcile loop's input.
Where pods get stuck, and how to debug it
The eleven-hop trace is also a debugging map. When a pod is "not running," it has stalled at one of those steps, and each stall has a recognizable signature.
Scheduler can't place it
Stuck at step 7. Either no node has the requested resources, every eligible node has a taint the pod doesn't tolerate, or a volume can't attach. kubectl describe pod shows scheduler events.
The node can’t pull the image
Stuck at step 10·CRI·PullImage. Wrong image name, wrong tag, missing imagePullSecret for a private registry, or registry is down. The kubelet retries with exponential backoff. Scheduling itself succeeded.
Container starts, then dies
Past step 10. The container starts and then exits, usually from bad config, a missing env var, a wrong entrypoint or an unreachable dependency. Logs are the main clue. kubectl logs --previous reads the dead instance.
Writing your own Kubernetes controller
The same watch-and-reconcile pattern is yours to use.
The control plane is a watch loop, a scheduler and a per-node reconciler, and the watch mechanism is available to your own code. Define a Custom Resource Definition, write a controller that watches it and reconciles toward the spec, and you have extended Kubernetes.
This is the operator pattern. cert-manager watches Certificate CRs and provisions TLS via ACME. prometheus-operator watches ServiceMonitor CRs and updates Prometheus configs. A Postgres operator watches Postgres CRs and runs an HA cluster. Each follows the same pattern as the pod flow above.
For what happens after the pod is running, see the K8s networking guide.
Pod startup latency: where the seconds go
From kubectl apply to the container's first byte served, the typical pod takes 5-30 seconds. Where the time goes:
- API server admission + etcd write
- ~50-200ms. Validates the spec, runs admission webhooks, writes to etcd, broadcasts the watch event.
- Scheduler decision
- ~100-500ms. Filter and score nodes. Typically the fastest hop; bottlenecks here usually mean an expensive admission webhook or scheduler plugin.
- Image pull
- ~1-30s. Dominant for cold image cache. Can be cut with image-streaming (containerd's lazy image-pull, AWS Soci) or by warming the local cache via DaemonSet.
- Container runtime sandbox creation
- ~100-500ms. The pause container, the network namespace, the cgroup tree, the storage mount. Usually fast, unless there are many init containers.
- CNI plugin
- ~50-300ms. Allocate the pod IP, plumb the routes. AWS VPC CNI was historically slow (~1-2s) on cold ENIs; prefix-delegation (2021) cut this to ~100ms.
- Container start
- ~50ms-N seconds. Depends entirely on application startup. JVM cold start ~5-10s; Go binary ~50ms; Python with imports ~200-500ms.
- Readiness probe success
- ~initialDelaySeconds + periodSeconds. The pod becomes Ready after the first passing probe, so a large initialDelaySeconds or periodSeconds adds directly to rollout time even if the container started in 50ms. A common reason a deploy is slower than expected.
Scale issues. In rolling deploys of thousands of pods, etcd's update rate and the kubelet's image-pull throughput become bottlenecks, and pod-start times in clusters of more than 5,000 nodes can stretch to minutes. Mitigations: pre-pull images via DaemonSet, use container-runtime image streaming, cap image size aggressively (Distroless and Alpine help).
Kubernetes has a lot of surface area, but the core is small: one read-write path through the API server, a consistent store behind it, and watches between the components. Most other concepts are one of those four pieces in a different role. To take each piece apart on its own, the Kubernetes internals notes cover the API server, scheduler, kubelet and etcd one at a time.
Further reading
- kubernetes.ioCluster ComponentsThe official architecture overview, with the standard name for every piece referenced in Part 01.
- kubernetes.ioScheduling FrameworkHow filter / score plugins compose. Useful before writing a custom scheduler or diagnosing an unscheduled pod.
- kubernetes/communityAPI ConventionsWhy every object has metadata.spec and metadata.status, and how the watch protocol's resourceVersion works. Worth reading before writing a controller.
- Semicolony guideK8s networkingWhat happens after step 11: pod-to-pod, pod-to-Service, Ingress. This guide ends at the CNI, and that one picks up there.
- Semicolony guideContainersNamespaces, cgroups, OCI: what runc does at step 10. Background for Part 05.