I wanted a straight answer for a cheap VPS: can a representative Spring Boot app live on 512 MB, and is there room left for something that actually watches it?
The result: 256 MB was too tight for this application. At 512 MB RAM plus a 256 MB swapfile, Spring Boot and StatLite completed the hour with zero restarts. StatLite ended at about 12 MiB RSS.
Spring Boot is not tiny. I did not try to make it tiny. The interesting part was the other process. In the successful run, StatLite ended around 12 MiB RSS while continuously polling both targets. In the separate no-swap test, Spring was OOM-killed and restarted while StatLite stayed up.
The setup that completed a clean hour was 512 MB RAM plus a 256 MB swapfile, with this JVM profile:
-Xms16m -Xmx64m -Xss256k -XX:+UseSerialGC
-Xmx64m is not a 64 MB process. At the end of that hour Spring RSS was about 167 MiB. The heap series in the dashboard sat around 40 to 50 MB. Both are true. They measure different things.
What I ran
Star Pulse is a Java 21 / Spring Boot 3.5.5 service with JPA, Hibernate, file-backed H2, embedded Tomcat, Actuator, a scheduler, and outbound HTTP to GitHub. It tracks star counts for a few repositories. I could have skipped Hibernate. Then I would have been measuring a different app.
The guest was Ubuntu 24.04, 1 vCPU, 5 GB disk. Spring and StatLite ran as separate systemd units on loopback. The 512 MB VM showed about 452 MiB of usable RAM. StatLite polled Spring and itself every 30 seconds.
Once both services had settled, I watched them for 60 minutes and sent 72 bounded HTTP requests, 12 to each of six Spring and StatLite endpoints. If Spring or StatLite was OOM-killed, crashed, restarted, or never became usable, I did not call the run clean. Sitting at high memory use was allowed. That is the point of a small machine.
I tried five configurations:
| Configuration | Result |
|---|---|
| 256 MB, no swap | OOM/restart loop, operationally unusable |
| 256 MB + 256 MB swap | Completed hour, but Spring OOM-restarted once and swap stayed effectively full |
| 256 MB + 256 MB swap, aggressive JVM limits | Failed startup with Metaspace allocation error |
| 512 MB, no swap | Completed hour, but Spring OOM-restarted once |
| 512 MB + 256 MB swap | Clean 60-minute run, zero restarts |
512 MB without swap was close
On 512 MB with no swap, the hour finished. All 72 requests returned 200. Spring still got OOM-killed in the first three minutes of the 60-minute observation window. The JVM had already been running before that window started. It restarted and then behaved. StatLite never restarted. Final RSS was 270,284 KiB for Spring and 5,300 KiB for StatLite.
That is a machine you can babysit. It is not one I would leave alone and call done.
512 MB plus a small swapfile
I kept the same app and the same JVM flags, and added a 256 MB swapfile (about 255 MiB usable). Spring reached health in 14 seconds. For the next hour:
- Spring restarts: 0
- StatLite restarts: 0
- 72 of 72 HTTP checks returned 200
- final Spring RSS: about 167 MiB (167,420 KiB)
- final StatLite RSS: about 12 MiB (12,556 KiB)
- swap used: about 160 MiB
- RAM still available: about 140 MiB
- 240 StatLite polls, 120 per target, 2,640 metric samples
Checkpoints from that run. Restart counts are Spring first, then StatLite.
| Checkpoint | RAM available | Swap used | Spring RSS | StatLite RSS | Restarts |
|---|---|---|---|---|---|
| Start | 81 MiB | 37 MiB | 244,024 KiB | 13,128 KiB | 0 / 0 |
| +90 seconds | 109 MiB | 97 MiB | 202,532 KiB | 13,588 KiB | 0 / 0 |
| +3 minutes, after traffic | 106 MiB | 140 MiB | 211,944 KiB | 10,044 KiB | 0 / 0 |
| +60 minutes | 140 MiB | 160 MiB | 167,420 KiB | 12,556 KiB | 0 / 0 |
H2 stayed at 44 KiB. StatLite's SQLite file grew from 548 KiB at the start of the observation to 736 KiB at the end, with seven-day retention. The JVM used SerialGC and a 256k thread stack, but those settings did not make the process the size of the heap. Metaspace, thread stacks, direct memory, and mapped files sit outside -Xmx. If you size the VPS from the heap flag alone, that is usually why the OOM killer shows up.
The monitor did not go down with Spring
When Spring was OOM-killed on the no-swap 512 MB run, StatLite stayed up and kept polling. Health, restarts, and memory were still on the dashboard. On a box this small I want that process separate. If it dies with Spring, it cannot tell you Spring just died.
Spring Boot is not a 12 MiB service. Watching it does not have to mean standing up Prometheus and Grafana next to it.
If a dashboard that small is useful on a box like this, star StatLite so other Spring Boot people can find it.
I left the box running after the hour. About 13 hours later both units were still active, still returning 200, still being polled. Swap was still in use. That is not a second timed run. It is just what it looked like the next day.
Don't copy this blindly
This is one app. Give it a remote Postgres, more schedulers, or a heavier set of starters and 512 MB may not be enough. I also would not size a fleet from one timed hour.
The smaller RAM rows above were a stretch test. I would not run this stack there. Checkpoints and method notes are in the public experiment repo.
Source
The method, the demo, and the numbers are in the public experiment repo. I left out private logs, credentials, and host paths.
Star Pulse is in the experiment tree. StatLite is the dashboard in the screenshots: one Go binary, SQLite on disk, Actuator in, charts out.