VPS or serverless. I did the math on a real project.
Every few months the question comes up, usually framed as ideology. This time I ran it as arithmetic, on a real project: a small internal API with a marketing site in front, about fifty thousand requests a month, steady. Three ways to host it, one spreadsheet — and one column nobody prices until the invoice arrives.
The contenders
- VPS: a Hetzner CX22-class box, €4.51 a month, twenty terabytes of traffic included.
- AWS: Lambda + API Gateway + DynamoDB on-demand, the architecture every tutorial starts with.
- Cloudflare Workers: free tier for this volume, $5 a month paid once you want the niceties.
The arithmetic
50k requests/month, 512MB, ~100ms average
Hetzner CX22 €4.51/mo flat
traffic included (20TB)
automated backups +€1.20
------
€5.71/mo, known forever
AWS Lambda
requests 50k × $0.20/M $0.01
compute ~1.4 GB-s ~$0.25
API GW 50k × $1/M $0.05
DynamoDB on-demand ~$0.10
logs ~$0.10
NAT gateway, if VPC $0.00 — or $32.40
------
without NAT ~$0.50/mo
with NAT ~$33/mo
Cloudflare Workers $0 free tier / $5 paid, 10M req includedCompute is noise at this scale — all three land within rounding distance of free. The bill is decided at the edges: egress and NAT.
- Egress. AWS charges roughly $0.09/GB past the first 100GB; Hetzner includes twenty terabytes; Cloudflare does not charge it at all. For an API that ships JSON, this line stays small — for anything that ships images or files, it becomes the whole bill.
- NAT. The moment the Lambda needs to reach something inside a VPC — say, a database — you rent a NAT gateway at around $32 a month to let two of your own things talk to each other. The most expensive “it just needs to reach the DB” in computing, and the reason half the serverless-bill horror stories exist.
The columns without prices
- Ops time. The VPS means patching and one box to babysit — Caddy keeps the certificates boring. Serverless means none of that, and means logs billed separately and quietly; CloudWatch at this volume costs more than the compute it observes.
- Cold starts. Irrelevant here. An internal API nobody is watching gets 200ms of startup once an hour, unnoticed.
- The dev loop. Running the thing locally against Lambda and DynamoDB is a container shuffle. Against the VPS it is just — the service. Six months of iteration pays this column daily.
- Spikes. Serverless eats a hundred-times traffic spike while you sleep; the VPS needs a conversation. For this project the spike never comes. For a launch-shaped project it is the whole decision.
The verdict
- Steady, small, database-flavored → VPS. €5.71, known forever, Postgres one millisecond away with no NAT rental in between.
- Event-driven, spiky, glue between managed services → serverless, and its bill stays honest exactly as long as it never needs a NAT.
- Global edge, barely any backend → Workers. Five dollars, done.
Where I landed
The API went to the VPS. It talks to Postgres, and I declined to rent a NAT gateway so my own code could say hello to its own database. The math was never about CPU-seconds — at real-world volumes those are a rounding error on every platform. It is about which edges your architecture forces you to pay for. Choose the shape that keeps the expensive columns at zero, and the ideology debate dissolves into a five-row spreadsheet.