
Memory is what small app plans are sized by, and it is the first limit a new SaaS app hits. Before you pick a 128 MB, 256 MB or 512 MB container for your app, it helps to know what the runtime itself takes before your code does anything. So we measured it: the same one-route JSON app in PHP, Python, plain Java, Spring Boot, .NET and a static nginx site, each in its own container, run 72 times.
Short version: a static site needs about 6 MiB, PHP about 13 MiB, and .NET, Python and plain Java land close together at 27 to 32 MiB. Spring Boot is in a different class at 164 MiB, but most of that is heap the JVM takes because it can, and it shrinks to 114 MiB as soon as the container has a limit.
How we measured
Each app answers GET / with {"hello":"world"} and nothing else, built on the official base image for its language and run as a non-root user. We started each container, waited for the first 200, sent 50 requests, let it idle for 3 seconds and read docker stats. Every runtime ran 3 times with no memory limit, 3 times with --memory 256m and 3 times with --memory 128m (swap off). Memory below is the median, in MiB as Docker reports it; PIDs counts processes and threads.
The stacks are the ones most people would start with: nginx serving one HTML file, PHP 8.3's built-in server with 4 workers, FastAPI on uvicorn (one process), the JDK's own com.sun.net.httpserver on Java 21, Spring Boot 3.5.6 with spring-boot-starter-web (Tomcat), and an ASP.NET Core 8 minimal API. Spring Boot ran twice: as the plain fat jar, and extracted with a class data sharing (CDS) archive.
The results
| Runtime | Stack | No limit | 256 MB limit | 128 MB limit | PIDs |
|---|---|---|---|---|---|
| nginx 1.29 | static file | 6.1 MiB | 6.1 MiB | 6.1 MiB | 7 |
| PHP 8.3 | built-in server, 4 workers | 12.8 MiB | 12.8 MiB | 12.8 MiB | 5 |
| .NET 8 | ASP.NET Core minimal API | 27.2 MiB | 26.9 MiB | 27.0 MiB | 23 |
| Python 3.12 | FastAPI 0.120 + uvicorn 0.38 | 31.6 MiB | 31.6 MiB | 31.6 MiB | 2 |
| Java 21 | JDK HttpServer | 32.3 MiB | 27.1 MiB | 28.1 MiB | 20 |
| Java 21 | Spring Boot 3.5 + CDS | 130.5 MiB | 98.9 MiB | 100.5 MiB | 38 |
| Java 21 | Spring Boot 3.5 | 163.8 MiB | 113.5 MiB | 113.7 MiB | 40 |

Every app served every request in all 72 runs, including Spring Boot at 128 MB. None was killed for running out of memory.
Why Java uses less when you give it less
The interpreted runtimes don't care about the limit: PHP, Python and nginx used the same memory with or without one. The JVM does care, because it sizes its heap from the memory it can see. With no limit our containers could see 11.69 GiB, so the JVM allowed itself a 2.92 GiB heap and Spring Boot spread out to 164 MiB. Under a limit it picks a much smaller maximum heap and the same app runs in 114 MiB:
So a Spring Boot number measured on a laptop with no limit says little about what it needs in production. Measure it with the limit you plan to use, or set the heap yourself with -XX:MaxRAMPercentage or -Xmx so it doesn't depend on the machine.
Workers multiply the Python number
The Python figure is one uvicorn process. Real deployments often run several workers for concurrency, and each one is a full copy of the interpreter and your imports. We ran the same image with --workers:
| uvicorn workers | Memory after 50 requests | PIDs |
|---|---|---|
| 1 (no supervisor) | 31.6 MiB | 2 |
| 2 | 87.0 MiB | 8 |
| 4 | 147.6 MiB | 13 |
That is roughly 30 MiB per extra worker plus a supervisor process. A FastAPI app with 4 workers already needs more memory than Spring Boot under a limit, before your own code and libraries are loaded. With async code, one worker is often enough for a small app; add workers when you measure that you need them.
Image size is a different story

The order flips for disk. PHP had the smallest memory use but the largest image: the official php:8.3-cli image is 743 MB unpacked (181 MB to download), against 212 MB for python:3.12-slim. Image size affects how fast a new server can pull and start your app and what you pay for registry storage, but it doesn't affect memory at run time. The CDS archive costs Spring Boot 80 MB more disk (576 MB) for its smaller memory and faster start.
What to take from it
For a small SaaS app, the runtime's own share of memory is: under 15 MiB for static sites and PHP, around 30 MiB for one .NET, Python or plain Java process, and 100 to 165 MiB for Spring Boot depending on the limit. Your code, libraries, caches and open database connections come on top. A rough plan: 128 MB is comfortable for PHP, single-worker Python and .NET minimal APIs; Spring Boot runs at 128 MB but has little room left for your app, so 256 MB or more is the safer start; and every extra Python worker should be counted at about 30 MiB of its own.
If you want to check your own app the same way, run it with the limit you plan to buy, send it some real requests and read docker stats --no-stream. The idle number after real traffic is the one to size by, not the number right after start.

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
