Bar chart of cold start, docker run to first HTTP 200: nginx static 0.32 s, PHP 8.3 0.33 s, Java 21 0.37 s, .NET 8 0.58 s, Python 3.12 0.67 s, Spring Boot 3.5 with CDS 1.17 s, Spring Boot 3.5 2.09 s

Apps that sleep when nobody uses them are cheap to run, but the first visitor after a quiet spell waits while the app starts. How long that wait is depends mostly on the runtime and framework you picked. We measured it: the time from docker run to the first successful response, for the same one-route app in six stacks, 9 times each.

Short version: PHP and plain Java answer in about a third of a second, barely slower than a static nginx site. .NET and FastAPI take 0.6 to 0.7 s. Spring Boot takes 2.1 s, and a class data sharing (CDS) archive brings that down to 1.17 s. Memory limits of 256 MB and 128 MB made no difference to start times.

How we measured

Each app answers GET / with {"hello":"world"}. The clock starts right before docker run -d and stops at the first 200 from a loop that polls every 10 ms. That includes Docker creating the container, the network and the port mapping, so it is the full wait a sleeping app's first visitor sees, minus any proxy in front. A static nginx site shows the floor: about 0.3 s of the total is Docker's own work.

text
machine     Docker Desktop, Docker Engine 29.6.1, WSL2 kernel 5.15.167, 6 CPUs
app         one route, GET / -> {"hello":"world"}, port 8080, non-root user
runs        3 with no limit, 3 with --memory 256m, 3 with --memory 128m
            per runtime, 72 runs in all, images already pulled
timer       start: before docker run -d    stop: first HTTP 200 (10 ms polling)

The results

RuntimeStackMedianFastestSlowest
nginx 1.29static file0.32 s0.28 s0.58 s
PHP 8.3built-in server0.33 s0.29 s0.34 s
Java 21JDK HttpServer0.37 s0.36 s0.43 s
.NET 8ASP.NET Core minimal API0.58 s0.55 s0.65 s
.NET 8same, published ReadyToRun0.59 s0.52 s0.70 s
Python 3.12FastAPI 0.120 + uvicorn 0.380.67 s0.63 s0.75 s
Java 21Spring Boot 3.5 + CDS archive1.17 s1.11 s1.36 s
Java 21Spring Boot 3.5, plain jar2.09 s1.78 s2.44 s

Take Docker's ~0.3 s away and the runtimes themselves need almost nothing for PHP, about 0.05 s for plain Java, about 0.25 s for .NET, 0.35 s for FastAPI and 1.8 s for Spring Boot. The JVM isn't slow to start; Spring is, because it scans the classpath, builds its application context and starts an embedded Tomcat.

Memory limits don't slow the start

RuntimeNo limit256 MB128 MB
PHP 8.30.33 s0.32 s0.33 s
Java 21, HttpServer0.37 s0.37 s0.36 s
.NET 80.63 s0.58 s0.57 s
Python 3.12, FastAPI0.73 s0.66 s0.65 s
Spring Boot 3.5 + CDS1.20 s1.12 s1.22 s
Spring Boot 3.52.10 s2.15 s1.84 s

Medians of 3 runs each. The differences are within run-to-run noise. Even Spring Boot at 128 MB, where the JVM picks a 64 MiB heap, started as fast as with no limit. A small container makes an app slower only when the app actually runs short of memory under load, not at start.

What made Spring Boot faster: a CDS archive

Class data sharing lets the JVM load pre-parsed classes from an archive instead of reading and verifying them from the jar on every start. Spring Boot 3.3 and later can extract the jar and write that archive in a training run that stops as soon as the application context is ready. These are the lines we added to the Dockerfile:

dockerfile
FROM eclipse-temurin:21-jre
COPY --from=build /src/target/app.jar /tmp/app.jar
RUN java -Djarmode=tools -jar /tmp/app.jar extract --destination /app && rm /tmp/app.jar
WORKDIR /app
# training run: start the app, write the archive, exit once the context is ready
RUN java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh -jar app.jar
CMD ["java", "-XX:SharedArchiveFile=app.jsa", "-jar", "app.jar"]

The result: 2.09 s down to 1.17 s (44% less), and idle memory from 164 MiB to 131 MiB with no limit, since shared classes take less heap. The price is 80 MB more image (576 MB against 496 MB) and a build step that starts your app once, so the training run must not need a database or other services, or must be able to start without them.

What didn't help: ReadyToRun for .NET

ReadyToRun compiles your assemblies ahead of time so less code is JIT-compiled at start. For our minimal API it changed nothing: 0.58 s against 0.59 s, the image stayed at 321 MB. That is expected for a tiny app, because the ASP.NET Core framework itself already ships precompiled, and the only code left to compile is a few lines of ours. ReadyToRun pays off in large apps with a lot of their own code; measure before you add it.

Where Python's time goes

Python itself starts in milliseconds; the imports are the cost. Importing fastapi and uvicorn alone took 0.38 to 0.42 s in the same image (three runs), against about 0.01 s for json. That is more than half of FastAPI's 0.67 s cold start. Every library you import at the top of main.py adds to it: if a heavy module (a PDF library, pandas, an ML model) is used by only one route, import it inside that route so the first request after a wake doesn't pay for it.

text
$ docker run --rm python-hello python -c "import time;t=time.perf_counter();import fastapi,uvicorn;print(round(time.perf_counter()-t,3))"
0.381
0.393
0.418

What to take from it

If your app sleeps when idle, the runtime decides most of the first visitor's wait. PHP, plain Java and static sites wake in about a third of a second including the container start, which nobody notices. .NET and FastAPI add a quarter to a third of a second more. Spring Boot's 2 seconds is noticeable, so either add a CDS archive (it halved the wait here), keep a Spring app awake, or measure what your own app's context costs. Your real app will be slower than a hello-world: database pools, migrations at start and big dependency trees all add up, so time your own docker run to first 200 the same way.

The systemi Deploy Java docs page: Spring Boot or any runnable jar on Java 21, detected by pom.xml or build.gradle, started with java -jar and the heap sized to the plan's memory
How systemi runs Java: Maven or Gradle builds on JDK 21 and the jar started with the heap sized to the plan's memory (systemi.in/docs/java).

systemi runs PHP, Python, Java, .NET and static apps, each in its own container with its own PostgreSQL database and a free subdomain.

See how deploys work