Bar chart of memory used idle and at peak under load, measured 31 October 2026: Go 4 / 16 MB, Redis 9 / 21 MB, PHP 8.3 + Apache 17 / 82 MB, Node 22 + Express 16 / 88 MB, FastAPI 1 worker 31 / 53 MB, Django 3 workers 96 / 97 MB, FastAPI 4 workers 104 / 142 MB, WordPress 90 / 276 MB, PostgreSQL 18 33 / 330 MB, MySQL 8.4 492 / 534 MB

"How much RAM do I need?" is the first question on any server order form, and most answers online are guesses copied from other guesses. So we measured. We ran ten common pieces of a small web app in containers, read their memory while idle, hammered each one for 30 seconds, and read it again. Everything below is from those runs on 31 October 2026, and the method is simple enough to repeat on your own server.

Short version: a typical small app (a Node, Python or Go backend, PostgreSQL and Redis) peaked at 370 to 520 MB in our tests. A default WordPress with MySQL peaked at about 830 MB, most of it MySQL. Add the operating system on top, and 1 GB is enough for the first kind with care, 2 GB is comfortable for either.

How we measured

Each app served one JSON endpoint that builds a list of 200 items per request, so the runtime does real allocation work instead of returning a constant. Each ran in its own Docker container limited to 2 vCPUs. We waited 20 seconds after start and took the median of five samples (idle), ran hey for 30 seconds with 50 concurrent connections while sampling every half second (peak), then waited 30 seconds and sampled again (idle after load). WordPress was a fresh 7.1.4 install serving its home page to 20 connections; PostgreSQL ran pgbench with 20 clients on a scale-10 database (about 150 MB); Redis ran redis-benchmark with 300,000 SET/GET of 256-byte values.

"Memory" here is what docker stats shows: the container's memory use minus inactive file cache. We also kept the anonymous memory (heap and stack, the part the kernel can't simply drop), which matters for PostgreSQL below. Host: Docker Desktop on a 6-core machine, Linux kernel 5.15. Versions: Node 22.23 + Express 4, Python 3.12 with FastAPI 0.115 / Uvicorn 0.32 and Django 5.1 / Gunicorn 23, PHP 8.3.36 with Apache (prefork, image defaults), Go 1.23, PostgreSQL 18, MySQL 8.4.13, Redis 7.4.12.

The results

StackIdlePeak under loadIdle after loadThroughput in our run
Go, net/http4 MB16 MB16 MB7,062 req/s
Redis 7.49 MB21 MB10 MB171,135 GET/s
PHP 8.3 + Apache (mod_php)17 MB82 MB47 MB4,115 req/s
Node 22 + Express16 MB88 MB22 MB4,395 req/s
FastAPI, 1 Uvicorn worker31 MB53 MB53 MB301 req/s
Django, 3 Gunicorn workers96 MB97 MB97 MB2,606 req/s
FastAPI, 4 Gunicorn/Uvicorn workers104 MB142 MB142 MB553 req/s
WordPress 7.1 (PHP 8.3 + Apache)90 MB276 MB167 MB23 req/s (home page)
PostgreSQL 18, defaults33 MB330 MB299 MB3,902 TPS (pgbench)
MySQL 8.4, defaults, empty434 MBn/an/an/a
MySQL 8.4 behind WordPress492 MB534 MB533 MBn/a

The throughput column is only there to show each stack was actually busy. It is not a benchmark: the load generator shared the machine and the endpoint is tiny. FastAPI's number is low because the handler is a plain def, which Uvicorn runs in a thread pool. Memory is what we are after here.

What the numbers say

Your app is usually the small part

Every app runtime in the test stayed under 150 MB at peak, and most under 100 MB. Go barely registers. Node and PHP jump under load and give memory back afterwards (Node went from 88 back to 22 MB; Apache kept some spare children at 47 MB). Python holds on to what it has grown to: FastAPI stayed at 53 MB after the load, the 4-worker setup at 142 MB.

Python cost grows per worker

Gunicorn starts one full Python process per worker. Django with 3 workers sat at 96 MB, about 32 MB each. FastAPI went from 31 MB with one worker to 104 MB with four, about 24 MB for each extra worker. The common advice of 2 x cores + 1 workers means 9 workers on a 4 vCPU server: roughly 220 to 290 MB for a simple app, more for one with heavy imports. Count workers before you count RAM.

The database is the big part, and its number needs reading

PostgreSQL showed 330 MB at peak, but only 35 MB of that was anonymous memory. The rest is its shared buffers (128 MB by default) and the kernel's cache of the database files it was reading. That cache is useful but elastic: on a small server the kernel shrinks it when apps need memory, and the database gets slower instead of crashing. So PostgreSQL fits in less RAM than the headline number suggests, as long as you leave some room for that cache.

MySQL 8.4 is different: 428 MB of its 434 MB idle was anonymous memory, before a single table existed. Two defaults account for most of it, the 128 MB InnoDB buffer pool and the Performance Schema. Restarting the same image with --performance-schema=OFF --innodb-buffer-pool-size=64M brought idle memory down to 195 MB. On a 1 GB server that one change is the difference between WordPress fitting and not.

WordPress grows with concurrency

Apache with mod_php runs a process per concurrent request. At 20 simultaneous visitors WordPress reached 276 MB, three times its idle size: about 9 MB per extra busy worker. With the Apache default of up to 150 workers, a burst of traffic on a small server can start more processes than RAM allows. Set MaxRequestWorkers to what fits; at the rate we saw, 30 workers comes to roughly 370 MB.

The operating system

A minimal Ubuntu 24.04 userland with systemd, journald, resolved, logind, rsyslog, cron and unattended-upgrades used 24 MB in our test container; the biggest single process was unattended-upgrades at 20 MB resident. A container shares the host's kernel, so that figure leaves out the kernel and its buffers, which a real server pays for. Canonical's own minimum for Ubuntu 24.04 cloud images is 1 GB of RAM (1.5 GB for the ISO installer). The totals below use our 24 MB; on a real server leave a couple of hundred MB more for the kernel, caches and the occasional apt upgrade.

Putting stacks together

Adding the peaks plus the 24 MB of system services gives the working set of a whole server. Peaks rarely all happen at once, so this is the cautious figure:

ServerPieces (peak, MB)Total at peakFits in
Go API + PostgreSQL16 + 330 + 24370 MB1 GB; 512 MB only with PostgreSQL tuned down
Node + PostgreSQL + Redis88 + 330 + 21 + 24463 MB1 GB
Django (3 workers) + PostgreSQL + Redis97 + 330 + 21 + 24472 MB1 GB
FastAPI (4 workers) + PostgreSQL + Redis142 + 330 + 21 + 24517 MB1 GB, tight; 2 GB comfortable
WordPress + MySQL, defaults276 + 534 + 24834 MB2 GB
WordPress + MySQL, Performance Schema off, 64 MB pool276 + about 237 + 24about 537 MB1 GB

For the tuned WordPress row we added MySQL's growth under load from the default run (42 MB) to the tuned 195 MB idle figure; we did not load-test the tuned setup separately. Leave room on top of every total for deploys and package installs, which run alongside everything else, and for your data growing.

Our sizing rules from these runs

RAMGood for
512 MBOne small app with SQLite or a database elsewhere; a Go or Node service; a test box. Add swap.
1 GBA small app with PostgreSQL and Redis on the same server, or WordPress with MySQL tuned down.
2 GBDefault WordPress + MySQL, a Python app with several workers and a database, or two small apps.
4 GB and upSeveral apps, a database whose working set is a few GB, background workers, builds on the server.

Measure your own server in two minutes

The numbers that matter are yours. On any Linux server:

bash
# overall: 'available' is what apps can still get without swapping
free -m

# the ten biggest processes by resident memory (KB)
ps -eo rss,comm --sort=-rss | head -11

# memory per systemd service, live
systemd-cgtop -m

# containers
docker stats --no-stream

Then put load on it the way we did and watch free -m from a second terminal:

bash
# 30 seconds, 50 connections against your own app
hey -z 30s -c 50 https://your-app.example/some/endpoint

If available drops under about 10% of RAM at peak, you need more memory or fewer workers. On small servers a swap file is cheap insurance against a crash during an unusual spike (it is not a substitute for RAM):

bash
sudo fallocate -l 1G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Where this lands on tport.cloud

Our smallest server is $1 a month for 0.5 vCPU, 512 MB and 10 GB, free for 30 days: enough for a Go or Node service or a test box. Shared servers start at $2 a month for 1 GB. The first standard server is $7.50 a month for 4 vCPU and 8 GB, which holds every combination in the table above several times over, with a database cache that never has to fight your app for room. If you would rather keep the database off the box, managed PostgreSQL is $30 a month for 12 GB (how it compares with DigitalOcean's). For what the same RAM costs elsewhere, see tport.cloud vs DigitalOcean Droplets.

Sources

Our own measurements, 31 October 2026 (Docker, 2 vCPU per container, hey 30 s / 50 connections, pgbench and redis-benchmark as described). Ubuntu: Ubuntu Server system requirements. Load tool: hey. Defaults referred to: PostgreSQL shared_buffers 128 MB, MySQL innodb_buffer_pool_size 128 MB, Apache prefork MaxRequestWorkers 150. tport.cloud prices from tport.cloud/pricing, Germany, 31 October 2026.

Start with the $1 server, free for 30 days, or take 8 GB for $7.50 a month. Traffic included, prepaid, no surprise bills.

See all servers