The Kubelet Checkpoint API  ·  Chapter 01  ·  measured, not recalled

One POST, One Tar

The first call returns 403, and the message names the exact permission you are missing. The second — after the RBAC grant and after the kubelet’s authorization cache expires — returns 200 and a JSON array containing one path. At that path is a 242 KB tar holding 21 CRIU image files, a filesystem diff and the container’s dumped spec. The container never stopped: still Running, restartCount=0, its counter still advancing.

measured on Ubuntu 24.04.5 aarch64, kernel 6.8.0-139-generic (QEMU/HVF, 4 vCPU / 6 GiB)
Kubernetes v1.36.4  ·  CRI-O 1.36.5  ·  CRIU 4.2.1  ·  runc 1.5.1  ·  cgroups v2
mode A — single-node kubeadm cluster built for this set; see the set README to reproduce it

Section 01

There is no feature gate to turn on

Most write-ups on this feature begin by telling you to set --feature-gates=ContainerCheckpoint=true. On any currently supported cluster that instruction is obsolete.

kubernetes/kubernetes@master · pkg/features/kube_features.gorung 2 · read the source
// owner: @adrianreber
// kep: https://kep.k8s.io/2008
// Enables container Checkpoint support in the kubelet
ContainerCheckpoint featuregate.Feature = "ContainerCheckpoint"
...
ContainerCheckpoint: {
    {Version: version.MustParse("1.25"), Default: false, PreRelease: featuregate.Alpha},
    {Version: version.MustParse("1.30"), Default: true,  PreRelease: featuregate.Beta},
},

Beta and on by default since 1.30, with no GA entry. The kubelet will tell you its own opinion if you ask it:

kubelet /metrics on the lab noderung 1 · measured
kubernetes_feature_enabled{name="ContainerCheckpoint",stage="BETA"} 1
Finding — the KEP is stale

keps/sig-node/2008-.../kep.yaml records stage: beta, latest-milestone: "v1.30", and milestone.stable: "v1.33", last updated 2024-02-08. Stable at 1.33 did not happen — the gate is still Beta at 1.36 and on master. When the KEP and the gate registry disagree about status, the registry is the one that ships.

Section 02

The endpoint

The route is registered on the kubelet’s authenticated port, and only when the gate is on.

kubernetes/kubernetes@master · pkg/kubelet/server/server.gorung 2 · read the source
checkpointPath      = "/checkpoint/"                      // line 117

// Only enable checkpoint API if the feature is enabled         // line ~692
    s.addMetricsBucketMatcher("checkpoint")
    ws.Path(checkpointPath).Produces(restful.MIME_JSON)
        To(s.checkpoint).
        Operation("checkpoint")

func (s *Server) checkpoint(request *restful.Request, response *restful.Response)   // line 1278

Shape: POST /checkpoint/{namespace}/{pod}/{container} on port 10250. There is no body and there are no options — which matters more than it looks, and chapter 04 shows why.

Section 03

The first call: 403, and a precise one

Calling with the API server’s own kubelet client certificate — the most privileged credential on the node — still fails.

first attempt, no extra RBACrung 1 · measured
Forbidden (user=kube-apiserver-kubelet-client, verb=create,
           resource=nodes, subresource(s)=[checkpoint])

HTTP 403
curl -sk -X POST --cert /etc/kubernetes/pki/apiserver-kubelet-client.crt --key ... https://localhost:10250/checkpoint/default/counter/app

The kubelet does not decide this itself — it delegates to the API server, which evaluates RBAC against a subresource of nodes that nothing grants by default. The error is unusually helpful: it names the user, the verb and the subresource, which is exactly the ClusterRole you need to write.

caller client cert POST kubelet :10250 1. authenticate cert 2. ask the API server SAR API server RBAC resource: nodes subresource: checkpoint verb: create 403 by default nothing grants it 200 with a ClusterRole granting nodes/checkpoint the kubelet caches the decision measured: the grant did not take effect until the cached denial expired
Two systems must agree before a byte is written. Authentication happens at the kubelet; authorization happens at the API server. The measured surprise is the box at the bottom — applying the ClusterRole and retrying immediately still returned 403.
Finding — the grant is not instant

After applying a ClusterRole granting create on nodes/checkpoint and binding it to kube-apiserver-kubelet-client, the very next call still returned 403. The kubelet caches authorization decisions, including denials. Only after waiting did the same call get through. If you are debugging this, do not conclude your RBAC is wrong from one immediate retry.

Section 04

The successful call

after the grant, once the cached denial expiredrung 1 · measured
{"items":["/var/lib/kubelet/checkpoints/checkpoint-counter_default-app-2026-09-13T17:38:36Z.tar"]}
HTTP 200

$ sudo ls -lh /var/lib/kubelet/checkpoints/
-rw------- 1 root root 242K Sep 13 17:38 checkpoint-counter_default-app-2026-09-13T17:38:36Z.tar

Three things to read off that. The response is a JSON array, not a file — the archive stays on the node and you fetch it by other means. The filename encodes pod, namespace, container and an RFC 3339 timestamp. And the mode is 0600, root:root, which chapter 05 argues is the only thing standing between that file and everything the process had in memory.

Section 05

Inside the tar

tar tf on the archiverung 1 · measured
top-level members
stats-dump
dump.log
config.dump
spec.dump
bind.mounts
rootfs-diff.tar
io.kubernetes.cri-o.LogPath

checkpoint/ — 21 CRIU image files
checkpoint/cgroup.img        checkpoint/ids-1.img
checkpoint/core-1.img        checkpoint/inventory.img
checkpoint/descriptors.json  checkpoint/ipcns-var-11.img
checkpoint/fdinfo-2.img      checkpoint/mm-1.img
checkpoint/files.img         checkpoint/mountpoints-13.img
checkpoint/fs-1.img          checkpoint/netns-10.img
                             checkpoint/pagemap-1.img   ...

total entries: 28
sudo tar tf /var/lib/kubelet/checkpoints/checkpoint-*.tar
RUNTIME STATE checkpoint/ · 21 files pages-*.img — memory contents core-1.img — registers, threads files.img — open descriptors netns-*.img — network namespace mm-1.img — address space map this is the part nothing else can capture FILESYSTEM DELTA rootfs-diff.tar files changed since the image bind.mounts — what was mounted DECLARED CONFIG spec.dump, config.dump the OCI spec, incl. env vars PROVENANCE dump.log — what CRIU did stats-dump — timings io.kubernetes.cri-o.LogPath 242 KB total, mode 0600 root:root on the node 28 entries, one file the container log is restored too — which is why a restored pod appears to remember what it printed before the checkpoint that detail caused a false reading during this work; see chapter 04’s closing note
Three kinds of content in one file. The teal column is the reason this feature exists — no backup or snapshot produces it. The violet box is worth remembering for chapter 05: the dumped OCI spec includes the container’s environment.

Section 06

The container never stopped

The most common assumption about checkpointing is that it stops the thing it captures. On this path it does not, and that is not configuration — it is written into the code.

cri-o@main · server/container_checkpoint.gorung 2 · read the source
opts := &lib.ContainerCheckpointOptions{
	TargetFile: req.GetLocation(),
	// For the forensic container checkpointing use case we
	// keep the container running after checkpointing it.
	KeepRunning: true,
}

KeepRunning is hardcoded, not read from the request — and the CRI message has no field that could carry it (chapter 02). Through the kubelet API you cannot ask for a destructive checkpoint.

immediately after a successful checkpointrung 1 · measured
Running restarts=0 started=2026-09-13T17:38:28Z
counter value now: 21      (still advancing)
What “non-destructive” does not mean

The process is still frozen while the dump runs — it is stopped, copied, and thawed. Chapter 03 measures that pause: 97 ms for a small container, 554 ms for one holding 256 MiB. For a latency-sensitive workload that pause is real, even though no restart is recorded.

Section 07

Further reading

Section 08

Closing note — what varies, and what was not verified

Durable versus run-specific

  • Durable: the 403 and its exact wording; that a ClusterRole on nodes/checkpoint is required; that the response is a JSON array of paths; mode 0600 root:root; the archive member layout; KeepRunning being hardcoded.
  • Run-specific: the 242 KB size, the 21-file count (CRIU emits per-process and per-namespace images, so it varies with the workload), the timestamp in the filename, and the counter value.

Not verified

  • Other authentication paths. Everything here used the API server’s kubelet client certificate from the node. A ServiceAccount token, or a call from off-node, was not tested.
  • The exact cache TTL. The denial clearly expired; the kubelet’s authorization-cache flags were not read back from the running process, so no duration is claimed.
  • containerd. Not tested here at all — chapter 02 shows from source why it cannot serve this call.
  • Multi-container pods and init containers. Only single-container pods were checkpointed.

Section 09

Spoken drills

A colleague says the checkpoint API needs a feature gate enabled. Is that right?

A strong answer hits

  • Not on anything current — ContainerCheckpoint is Beta and default-on since 1.30
  • It was alpha and off from 1.25 to 1.29, which is what older posts describe
  • The kubelet publishes the answer as a metric you can just read
  • The honest half: it is still Beta at 1.36 despite the KEP targeting stable at 1.33 — so it is on, but it is not a stability guarantee

check against §01

You get a 403 from the checkpoint endpoint using a cluster-admin-equivalent certificate. Walk through why.

A strong answer hits

  • The kubelet authenticates locally but delegates authorization to the API server
  • It needs create on the nodes/checkpoint subresource, which no default role grants
  • The error message names user, verb and subresource — it is the ClusterRole spec
  • The honest half: after granting it, the next call can still 403 because the kubelet caches the denial — measured here. Retrying once and concluding the RBAC is wrong is the trap

check against §03

Does taking a checkpoint disturb the running workload?

A strong answer hits

  • It does not stop or restart it — CRI-O hardcodes KeepRunning: true for the forensic use case
  • Measured: Running, restartCount=0, counter still advancing afterwards
  • You could not request otherwise anyway; the CRI message has no such field
  • The honest half: it is frozen for the duration of the dump — ~97 ms small, ~554 ms at 256 MiB. Non-destructive is not the same as non-disruptive

check against §06

Someone hands you a checkpoint tar and asks what they can learn from it. What is in there?

A strong answer hits

  • CRIU images: memory pages, registers, open descriptors, namespaces
  • A rootfs diff — files the container changed since its image
  • The dumped OCI spec and config, including the container’s environment
  • The container’s log path, so the prior log travels with it
  • The honest half: that combination means the file is as sensitive as a core dump plus the pod’s env — chapter 05 recovers a Secret from one

check against §05