CRaC and Checkpoint/Restore: The Third Startup Strategy
Java has a startup problem, and most of us have felt it. A service can take seconds to become ready, yet modern platforms start and stop instances all day. Most teams attack this with one of two well-known strategies. However, a third one has quietly matured, and it deserves a closer look.
Why a JVM Starts Slowly
Before comparing strategies, it helps to see what the JVM actually does at startup. According to the authors of JEP 483, a typical server application scans the same JAR files, loads and links the same classes, runs the same static initializers, and uses reflection to configure the same objects on every single start. In addition, the JIT compiler needs time to find the hot code. As a result, a fresh instance is slow at first and fast only later.
The First Two Strategies in Brief
Strategy One: Do Less Work at Startup
The first strategy keeps the normal JVM but moves work earlier. Class-data sharing has done this for years. More recently, Project Leyden added an ahead-of-time cache in JDK 24 through JEP 483. You run the application once for training, store the loaded and linked classes, and reuse them on later starts. For Spring PetClinic 3.2.0, the JEP reports 4.486 seconds on JDK 23 and 2.604 seconds with the cache, which is a 42% improvement. The cache file is 130 MB, but no code changes are needed.
Strategy Two: Compile Ahead of Time
The second strategy compiles the application into a native executable, for example with GraalVM Native Image. Startup becomes very fast. On the other hand, a Foojay overview of CRaC points out the cost: once code is statically compiled, you lose the runtime optimizations that the JVM can apply.
Strategy Three: Snapshot a Running JVM
The Core Idea
The third strategy asks a different question. Why start at all? Instead, you start the application once, let it warm up, and then take a snapshot of the whole process. Later, you restore that snapshot. The restored JVM continues from the moment of the checkpoint, with loaded classes and compiled code included. The CRaC documentation describes exactly this: an image created from a running instance, and a restore that carries on from that point.
Where the Coordination Comes In
Checkpoint tools for Linux already existed. CRaC, short for Coordinated Restore at Checkpoint, adds the missing coordination. The JVM can only create a checkpoint if its whole state fits into the image, and open files or sockets do not. Therefore, CRaC notifies the application before the checkpoint and after the restore, so that it can close and reopen such resources. The project was started at Azul, and its first engine is built on CRIU, the Linux checkpoint/restore tool.
A Minimal Example You Can Run
The example below needs a 64-bit Linux machine. It downloads a CRaC-enabled JDK 24 build from the CRaC project (roughly 400 MB), then runs a tiny program that registers both hooks. Because the default engine is based on CRIU, it runs with root privileges, as explained in the Azul engine documentation.
curl -L -o crac-jdk.tar.gz \
"https://github.com/CRaC/openjdk-builds/releases/download/24-crac%2B20/openjdk-24-crac+20_linux-x64.tar.gz"
sudo tar zxf crac-jdk.tar.gz
export JAVA_HOME="$PWD/openjdk-24-crac+20_linux-x64"
mkdir crac-demo && cd crac-demo
cat > Hello.java <<'EOF'
import jdk.crac.Context;
import jdk.crac.Core;
import jdk.crac.Resource;
public class Hello implements Resource {
public static void main(String[] args) throws Exception {
Core.getGlobalContext().register(new Hello());
long sum = 0;
for (int i = 0; i < 300_000_000; i++) {
sum += i % 7;
}
System.out.println("Expensive start-up work done: " + sum);
Core.checkpointRestore();
System.out.println("Back from the snapshot, ready to serve");
}
@Override
public void beforeCheckpoint(Context<? extends Resource> c) {
System.out.println("Before checkpoint: close files and sockets");
}
@Override
public void afterRestore(Context<? extends Resource> c) {
System.out.println("After restore: reopen files and sockets");
}
}
EOF
"$JAVA_HOME/bin/javac" Hello.java
sudo "$JAVA_HOME/bin/java" -XX:CRaCCheckpointTo=cr Hello
sudo "$JAVA_HOME/bin/java" -XX:CRaCRestoreFrom=cr
The first java command does the expensive work, runs the before-checkpoint hook, writes the image into the cr folder, and exits. The second command restores that image. Because a restore continues from the checkpoint, the expensive loop is not repeated.
What the Numbers Say
Numbers make the idea concrete. Azul measured Spring PetClinic with Spring Boot 3.2 on Azul Zulu, and the results are published in its documentation. An automatic checkpoint, taken just before the Spring lifecycle starts, brought startup from 4099 ms down to 392 ms, roughly ten times faster. A manual checkpoint of a fully started application reached 75 ms, which is roughly 55 times faster.

Likewise, the Helidon team reported large gains when it added CRaC support in version 4.2. Helidon SE dropped from 383 ms to 20 ms, while Helidon MP fell from 1597 ms to 32 ms.

One warning is needed here. These figures come from different benchmarks and machines, so they should not be compared with the 4.486 s and 2.604 s from the JEP 483 section. Nevertheless, the direction is clear.
Where You Can Use It Today
Framework support is the main reason CRaC is practical. Spring Framework 6.1 and Spring Boot 3.2 integrate with it, and the Spring reference explains two modes. With -Dspring.context.checkpoint=onRefresh, a checkpoint is taken automatically at startup. Alternatively, you can trigger a checkpoint later with jcmd, so that the image holds a warmed-up JVM. In both cases, Spring stops lifecycle beans before the checkpoint and starts them again after the restore.
Other frameworks followed. According to the CRaC project, Quarkus has had basic support since 2.10.0, and Micronaut and Helidon provide it as well. In the cloud, AWS built Lambda SnapStart on the same idea. It was announced at re:Invent 2022 for Java 11, and its runtime hooks use the CRaC API.
The Catches
Of course, snapshots come with rules. A checkpoint freezes everything the process holds, which creates several practical risks. The table below sums them up.
| What is frozen | Why it matters | What to do |
|---|---|---|
| Files and sockets | They cannot be stored in the image. | Close them before the checkpoint and reopen them after the restore. |
| Memory contents | Secrets and configuration the JVM has seen end up in the image files. | Treat images as sensitive and control where they are stored. |
| Scheduled tasks | With on-demand restore, missed fixedRate runs in Spring are executed after the restore. | Prefer fixedDelay or cron expressions. |
| Random seeds and unique IDs | Values captured in a snapshot can repeat across restored copies. | Refresh them in the after-restore hook. |
| Platform | Linux only for now, and the CRIU engine needs root or extra capabilities in containers. | Consider the Warp engine in Azul Zulu, which needs no extra privileges. |
In short, the cost moves from startup time to operations. Someone must create, store, and protect the images.
Choosing Between the Three
So, which strategy fits? The answer depends on how much change your team can accept.
| Strategy | Code impact | Main benefit | Main trade-off |
|---|---|---|---|
| AOT cache (JEP 483) | None, only a training run. | Works with existing applications. | Smaller gain, 130 MB cache for PetClinic. |
| Native image | Build changes. | Very fast start. | Loses JVM runtime optimizations. |
| CRaC | Hooks for resources, often handled by the framework. | Fast start with a warmed-up JVM. | Linux only, image management. |
Checklist before you try CRaC
- Is your target platform Linux?
- Can your containers grant the needed capabilities, or can you use the Warp engine?
- Which files, sockets, and schedulers need hooks?
- Who may read the image files, and where are they stored?
- Will checkpoint and restore run on CPUs with the same features?
What We Have Learned
Java startup is slow because the JVM repeats the same loading, linking, and warm-up work on every start. The first strategy, such as the JEP 483 AOT cache, does less of that work and needs no code changes. The second strategy, native compilation, removes most of it but gives up JVM runtime optimizations. The third strategy, CRaC, skips the startup entirely by restoring a warmed-up snapshot. Published figures show PetClinic dropping from 4099 ms to 75 ms and Helidon MP from 1597 ms to 32 ms. However, the approach is Linux only, it requires hooks for files and sockets, and it puts secrets into image files. Therefore, the best choice depends on your platform, your tolerance for operational work, and how much startup time you really need to save.

